Skip to content

Commit 04fad91

Browse files
authored
[PM-23645] Update cards to support pin and valid_from (#374)
## ๐ŸŽŸ๏ธ Tracking <!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. --> https://bitwarden.atlassian.net/browse/PM-23645 ## ๐Ÿ“” Objective <!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. --> Wrap up cards by supporting the fields not exposed in our data model as custom fields. ## โฐ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## ๐Ÿฆฎ Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - ๐Ÿ‘ (`:+1:`) or similar for great changes - ๐Ÿ“ (`:memo:`) or โ„น๏ธ (`:information_source:`) for notes or general info - โ“ (`:question:`) for questions - ๐Ÿค” (`:thinking:`) or ๐Ÿ’ญ (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - ๐ŸŽจ (`:art:`) for suggestions / improvements - โŒ (`:x:`) or โš ๏ธ (`:warning:`) for more significant problems or concerns needing attention - ๐ŸŒฑ (`:seedling:`) or โ™ป๏ธ (`:recycle:`) for future improvements or indications of technical debt - โ› (`:pick:`) for minor or nitpick changes
1 parent 4d1f8f5 commit 04fad91

File tree

4 files changed

+92
-9
lines changed

4 files changed

+92
-9
lines changed

โ€Žcrates/bitwarden-exporters/src/cxf/card.rs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use chrono::Month;
77
use credential_exchange_format::{Credential, CreditCardCredential, EditableFieldYearMonth};
88
use num_traits::FromPrimitive;
99

10-
use crate::Card;
10+
use crate::{cxf::editable_field::create_field, Card, Field};
1111

1212
impl From<Card> for Vec<Credential> {
1313
fn from(value: Card) -> Self {
@@ -57,6 +57,23 @@ impl From<&CreditCardCredential> for Card {
5757
}
5858
}
5959

60+
pub(super) fn to_card(credential: &CreditCardCredential) -> (Card, Vec<Field>) {
61+
let card = credential.into();
62+
63+
let fields = [
64+
credential.pin.as_ref().map(|v| create_field("PIN", v)),
65+
credential
66+
.valid_from
67+
.as_ref()
68+
.map(|v| create_field("Valid From", v)),
69+
]
70+
.into_iter()
71+
.flatten()
72+
.collect();
73+
74+
(card, fields)
75+
}
76+
6077
/// Sanitize credit card brand
6178
///
6279
/// Performs a fuzzy match on the string to find a matching brand. By converting to lowercase and
@@ -83,6 +100,7 @@ fn sanitize_brand(value: &str) -> Option<String> {
83100

84101
#[cfg(test)]
85102
mod tests {
103+
use bitwarden_vault::FieldType;
86104
use chrono::Month;
87105
use credential_exchange_format::EditableFieldYearMonth;
88106

@@ -150,23 +168,47 @@ mod tests {
150168
full_name: Some("John Doe".to_string().into()),
151169
card_type: Some("Visa".to_string().into()),
152170
verification_number: Some("123".to_string().into()),
153-
pin: None,
171+
pin: Some("4567".to_string().into()),
154172
expiry_date: Some(
155173
EditableFieldYearMonth {
156174
year: 2025,
157175
month: Month::December,
158176
}
159177
.into(),
160178
),
161-
valid_from: None,
179+
valid_from: Some(
180+
EditableFieldYearMonth {
181+
year: 2024,
182+
month: Month::January,
183+
}
184+
.into(),
185+
),
162186
};
163187

164-
let card: Card = (&credit_card).into();
188+
let (card, fields) = to_card(&credit_card);
165189
assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
166190
assert_eq!(card.exp_month, Some("12".to_string()));
167191
assert_eq!(card.exp_year, Some("2025".to_string()));
168192
assert_eq!(card.code, Some("123".to_string()));
169193
assert_eq!(card.brand, Some("Visa".to_string()));
170194
assert_eq!(card.number, Some("4111111111111111".to_string()));
195+
196+
assert_eq!(
197+
fields,
198+
vec![
199+
Field {
200+
name: Some("PIN".to_string()),
201+
value: Some("4567".to_string()),
202+
r#type: FieldType::Hidden as u8,
203+
linked_id: None,
204+
},
205+
Field {
206+
name: Some("Valid From".to_string()),
207+
value: Some("2024-01".to_string()),
208+
r#type: FieldType::Text as u8,
209+
linked_id: None,
210+
},
211+
]
212+
)
171213
}
172214
}

โ€Žcrates/bitwarden-exporters/src/cxf/editable_field.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use bitwarden_vault::FieldType;
22
use credential_exchange_format::{
33
EditableField, EditableFieldBoolean, EditableFieldConcealedString, EditableFieldCountryCode,
44
EditableFieldDate, EditableFieldString, EditableFieldWifiNetworkSecurityType,
5+
EditableFieldYearMonth,
56
};
67

78
use crate::Field;
@@ -74,6 +75,18 @@ impl EditableFieldToField for EditableField<EditableFieldDate> {
7475
}
7576
}
7677

78+
impl EditableFieldToField for EditableField<EditableFieldYearMonth> {
79+
const FIELD_TYPE: FieldType = FieldType::Text;
80+
81+
fn field_value(&self) -> String {
82+
format!(
83+
"{:04}-{:02}",
84+
self.value.year,
85+
self.value.month.number_from_month()
86+
)
87+
}
88+
}
89+
7790
/// Convert WiFi security type enum to human-readable string
7891
fn security_type_to_string(security_type: &EditableFieldWifiNetworkSecurityType) -> &str {
7992
use EditableFieldWifiNetworkSecurityType::*;
@@ -257,4 +270,31 @@ mod tests {
257270
}
258271
);
259272
}
273+
274+
#[test]
275+
fn test_create_field_year_month() {
276+
use chrono::Month;
277+
278+
let editable_field = EditableField {
279+
id: None,
280+
label: None,
281+
value: EditableFieldYearMonth {
282+
year: 2025,
283+
month: Month::December,
284+
},
285+
extensions: None,
286+
};
287+
288+
let field = create_field("Card Expiry", &editable_field);
289+
290+
assert_eq!(
291+
field,
292+
Field {
293+
name: Some("Card Expiry".to_string()),
294+
value: Some("2025-12".to_string()),
295+
r#type: FieldType::Text as u8,
296+
linked_id: None,
297+
}
298+
);
299+
}
260300
}

โ€Žcrates/bitwarden-exporters/src/cxf/import.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ use credential_exchange_format::{
99
use crate::{
1010
cxf::{
1111
api_key::api_key_to_fields,
12+
card::to_card,
1213
identity::{
1314
address_to_identity, drivers_license_to_identity, identity_document_to_identity,
1415
passport_to_identity, person_name_to_identity,
1516
},
16-
login::{to_fields, to_login},
17+
login::to_login,
1718
note::extract_note_content,
1819
wifi::wifi_to_fields,
1920
CxfError,
@@ -81,10 +82,9 @@ pub(super) fn parse_item(value: Item) -> Vec<ImportingCipher> {
8182

8283
// Credit Card credentials
8384
if let Some(credit_card) = grouped.credit_card.first() {
84-
add_item(
85-
CipherType::Card(Box::new(credit_card.into())),
86-
scope.map(to_fields).unwrap_or_default(),
87-
);
85+
let (card, fields) = to_card(credit_card);
86+
87+
add_item(CipherType::Card(Box::new(card)), fields);
8888
}
8989

9090
// Helper for creating SecureNote cipher type

โ€Žcrates/bitwarden-exporters/src/cxf/login.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ fn to_uris(scope: &CredentialScope) -> Vec<LoginUri> {
101101
/// Converts a `CredentialScope` to a vector of `Field` objects.
102102
///
103103
/// This is used for non-login credentials.
104+
#[allow(unused)]
104105
pub(super) fn to_fields(scope: &CredentialScope) -> Vec<Field> {
105106
let urls = scope.urls.iter().enumerate().map(|(i, u)| Field {
106107
name: Some(format!("Url {}", i + 1)),

0 commit comments

Comments
ย (0)