diff --git a/apps/mobile-app/app/(tabs)/settings/import-export.tsx b/apps/mobile-app/app/(tabs)/settings/import-export.tsx
index f78f2afe7..dc0632b8b 100644
--- a/apps/mobile-app/app/(tabs)/settings/import-export.tsx
+++ b/apps/mobile-app/app/(tabs)/settings/import-export.tsx
@@ -35,6 +35,12 @@ interface IItemCsvRecord {
AliasLastName: string;
AliasNickName: string;
AliasBirthDate: string;
+ CardholderName: string;
+ CardNumber: string;
+ CardExpiryMonth: string;
+ CardExpiryYear: string;
+ CardCvv: string;
+ CardPin: string;
Notes: string;
CreatedAt: string;
UpdatedAt: string;
@@ -127,6 +133,12 @@ export default function ImportExportScreen(): React.ReactNode {
const aliasLastName = getFieldValueAsString(item, 'alias.lastName');
const aliasBirthdate = getFieldValueAsString(item, 'alias.birthdate');
const notes = getFieldValueAsString(item, 'notes.content');
+ const cardholderName = getFieldValueAsString(item, 'card.cardholder_name');
+ const cardNumber = getFieldValueAsString(item, 'card.number');
+ const cardExpiryMonth = getFieldValueAsString(item, 'card.expiry_month');
+ const cardExpiryYear = getFieldValueAsString(item, 'card.expiry_year');
+ const cardCvv = getFieldValueAsString(item, 'card.cvv');
+ const cardPin = getFieldValueAsString(item, 'card.pin');
// Parse birthdate to formatted string (server expects MM/DD/YYYY format in CSV)
const formattedBirthDate = aliasBirthdate ? formatDate(aliasBirthdate) : '';
@@ -144,6 +156,12 @@ export default function ImportExportScreen(): React.ReactNode {
AliasLastName: aliasLastName,
AliasNickName: '', // NickName is no longer stored as a separate field
AliasBirthDate: formattedBirthDate,
+ CardholderName: cardholderName,
+ CardNumber: cardNumber,
+ CardExpiryMonth: cardExpiryMonth,
+ CardExpiryYear: cardExpiryYear,
+ CardCvv: cardCvv,
+ CardPin: cardPin,
Notes: notes,
CreatedAt: formatDate(item.CreatedAt),
UpdatedAt: formatDate(item.UpdatedAt)
@@ -166,6 +184,12 @@ export default function ImportExportScreen(): React.ReactNode {
'AliasLastName',
'AliasNickName',
'AliasBirthDate',
+ 'CardholderName',
+ 'CardNumber',
+ 'CardExpiryMonth',
+ 'CardExpiryYear',
+ 'CardCvv',
+ 'CardPin',
'Notes',
'CreatedAt',
'UpdatedAt'
@@ -202,6 +226,12 @@ export default function ImportExportScreen(): React.ReactNode {
escapeCsvValue(record.AliasLastName),
escapeCsvValue(record.AliasNickName),
escapeCsvValue(record.AliasBirthDate),
+ escapeCsvValue(record.CardholderName),
+ escapeCsvValue(record.CardNumber),
+ escapeCsvValue(record.CardExpiryMonth),
+ escapeCsvValue(record.CardExpiryYear),
+ escapeCsvValue(record.CardCvv),
+ escapeCsvValue(record.CardPin),
escapeCsvValue(record.Notes),
escapeCsvValue(record.CreatedAt),
escapeCsvValue(record.UpdatedAt)
diff --git a/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs b/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs
index 66e7b2aa0..9c0541a5a 100644
--- a/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs
+++ b/apps/server/Tests/AliasVault.UnitTests/Utilities/ImportExportTests.cs
@@ -79,6 +79,115 @@ public class ImportExportTests
});
}
+ ///
+ /// Test case for round-tripping a credit card item through CSV export and import.
+ /// Verifies that card-specific columns are populated on export and that the
+ /// importer recognizes the item as a credit card.
+ ///
+ /// Async task.
+ [Test]
+ public async Task ImportCreditCardItemFromCsv()
+ {
+ // Arrange
+ var loginItem = new Item
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000001"),
+ Name = "Login service",
+ ItemType = ItemType.Login,
+ CreatedAt = DateTime.Now,
+ UpdatedAt = DateTime.Now,
+ };
+ AddFieldValue(loginItem, FieldKey.LoginUsername, "loginuser");
+ AddFieldValue(loginItem, FieldKey.LoginPassword, "loginpass");
+
+ var cardItem = new Item
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000002"),
+ Name = "My Visa",
+ ItemType = ItemType.CreditCard,
+ CreatedAt = DateTime.Now,
+ UpdatedAt = DateTime.Now,
+ };
+ AddFieldValue(cardItem, FieldKey.CardCardholderName, "John Doe");
+ AddFieldValue(cardItem, FieldKey.CardNumber, "4111111111111111");
+ AddFieldValue(cardItem, FieldKey.CardExpiryMonth, "12");
+ AddFieldValue(cardItem, FieldKey.CardExpiryYear, "2030");
+ AddFieldValue(cardItem, FieldKey.CardCvv, "123");
+ AddFieldValue(cardItem, FieldKey.CardPin, "9876");
+ AddFieldValue(cardItem, FieldKey.NotesContent, "Card notes");
+
+ var csvContent = ItemCsvService.ExportItemsToCsv([loginItem, cardItem]);
+ var csvString = System.Text.Encoding.Default.GetString(csvContent);
+
+ // Assert: header includes the new card columns
+ var headerLine = csvString.Split('\n')[0];
+ Assert.Multiple(() =>
+ {
+ Assert.That(headerLine, Does.Contain("CardholderName"));
+ Assert.That(headerLine, Does.Contain("CardNumber"));
+ Assert.That(headerLine, Does.Contain("CardExpiryMonth"));
+ Assert.That(headerLine, Does.Contain("CardExpiryYear"));
+ Assert.That(headerLine, Does.Contain("CardCvv"));
+ Assert.That(headerLine, Does.Contain("CardPin"));
+ });
+
+ // Act
+ var importedCredentials = await ItemCsvService.ImportItemsFromCsv(csvString);
+
+ // Assert
+ Assert.That(importedCredentials, Has.Count.EqualTo(2));
+
+ var importedLogin = importedCredentials.First(c => c.ServiceName == "Login service");
+ Assert.Multiple(() =>
+ {
+ Assert.That(importedLogin.ItemType, Is.Null, "Login items should not have ItemType set by CSV importer.");
+ Assert.That(importedLogin.Creditcard, Is.Null);
+ Assert.That(importedLogin.Username, Is.EqualTo("loginuser"));
+ });
+
+ var importedCard = importedCredentials.First(c => c.ServiceName == "My Visa");
+ Assert.Multiple(() =>
+ {
+ Assert.That(importedCard.ItemType, Is.EqualTo(ImportedItemType.Creditcard));
+ Assert.That(importedCard.Creditcard, Is.Not.Null);
+ Assert.That(importedCard.Creditcard!.CardholderName, Is.EqualTo("John Doe"));
+ Assert.That(importedCard.Creditcard.Number, Is.EqualTo("4111111111111111"));
+ Assert.That(importedCard.Creditcard.ExpiryMonth, Is.EqualTo("12"));
+ Assert.That(importedCard.Creditcard.ExpiryYear, Is.EqualTo("2030"));
+ Assert.That(importedCard.Creditcard.Cvv, Is.EqualTo("123"));
+ Assert.That(importedCard.Creditcard.Pin, Is.EqualTo("9876"));
+ Assert.That(importedCard.Notes, Is.EqualTo("Card notes"));
+ });
+ }
+
+ ///
+ /// Verifies that CSV files exported by older versions (without credit card columns)
+ /// can still be imported successfully.
+ ///
+ /// Async task.
+ [Test]
+ public async Task ImportLegacyCsvWithoutCreditCardColumns()
+ {
+ // Arrange: a CSV with the pre-card column set only.
+ var legacyCsv =
+ "ServiceName,FolderPath,ServiceUrl,Username,CurrentPassword,AliasEmail,TwoFactorSecret,AliasGender,AliasFirstName,AliasLastName,AliasNickName,AliasBirthDate,Notes,CreatedAt,UpdatedAt\n"
+ + "Old Service,,https://old.example,olduser,oldpass,,,,,,,,,2024-01-01 00:00:00,2024-01-01 00:00:00\n";
+
+ // Act
+ var importedCredentials = await ItemCsvService.ImportItemsFromCsv(legacyCsv);
+
+ // Assert
+ Assert.That(importedCredentials, Has.Count.EqualTo(1));
+ var credential = importedCredentials[0];
+ Assert.Multiple(() =>
+ {
+ Assert.That(credential.ServiceName, Is.EqualTo("Old Service"));
+ Assert.That(credential.Username, Is.EqualTo("olduser"));
+ Assert.That(credential.ItemType, Is.Null);
+ Assert.That(credential.Creditcard, Is.Null);
+ });
+ }
+
///
/// Test case for importing credentials from Bitwarden CSV and ensuring all values are present.
///
diff --git a/apps/server/Utilities/AliasVault.ImportExport/ItemCsvService.cs b/apps/server/Utilities/AliasVault.ImportExport/ItemCsvService.cs
index 4f426c125..faf6b9cfe 100644
--- a/apps/server/Utilities/AliasVault.ImportExport/ItemCsvService.cs
+++ b/apps/server/Utilities/AliasVault.ImportExport/ItemCsvService.cs
@@ -48,6 +48,12 @@ public static class ItemCsvService
Notes = GetFieldValue(item, FieldKey.NotesContent),
CreatedAt = item.CreatedAt,
UpdatedAt = item.UpdatedAt,
+ CardholderName = GetFieldValue(item, FieldKey.CardCardholderName),
+ CardNumber = GetFieldValue(item, FieldKey.CardNumber),
+ CardExpiryMonth = GetFieldValue(item, FieldKey.CardExpiryMonth),
+ CardExpiryYear = GetFieldValue(item, FieldKey.CardExpiryYear),
+ CardCvv = GetFieldValue(item, FieldKey.CardCvv),
+ CardPin = GetFieldValue(item, FieldKey.CardPin),
};
records.Add(record);
@@ -117,6 +123,20 @@ public static class ItemCsvService
FolderPath = string.IsNullOrWhiteSpace(record.FolderPath) ? null : record.FolderPath,
};
+ if (HasCardData(record))
+ {
+ credential.ItemType = ImportedItemType.Creditcard;
+ credential.Creditcard = new ImportedCreditcard
+ {
+ CardholderName = record.CardholderName,
+ Number = record.CardNumber,
+ ExpiryMonth = record.CardExpiryMonth,
+ ExpiryYear = record.CardExpiryYear,
+ Cvv = record.CardCvv,
+ Pin = record.CardPin,
+ };
+ }
+
credentials.Add(credential);
}
@@ -161,6 +181,21 @@ public static class ItemCsvService
?.Value ?? string.Empty;
}
+ ///
+ /// Returns true if the CSV record has any credit card field populated.
+ ///
+ /// The CSV record to inspect.
+ /// True if any card field has a non-empty value.
+ private static bool HasCardData(ItemCsvRecord record)
+ {
+ return !string.IsNullOrWhiteSpace(record.CardholderName)
+ || !string.IsNullOrWhiteSpace(record.CardNumber)
+ || !string.IsNullOrWhiteSpace(record.CardExpiryMonth)
+ || !string.IsNullOrWhiteSpace(record.CardExpiryYear)
+ || !string.IsNullOrWhiteSpace(record.CardCvv)
+ || !string.IsNullOrWhiteSpace(record.CardPin);
+ }
+
///
/// Parses a birth date string to a DateTime.
///
@@ -248,6 +283,42 @@ public class ItemCsvRecord
///
public DateTime? AliasBirthDate { get; set; } = null;
+ ///
+ /// Gets or sets the credit card cardholder name.
+ /// Empty for non-credit-card items.
+ ///
+ public string CardholderName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the credit card number.
+ /// Empty for non-credit-card items.
+ ///
+ public string CardNumber { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the credit card expiry month (01-12).
+ /// Empty for non-credit-card items.
+ ///
+ public string CardExpiryMonth { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the credit card expiry year (e.g., "2028").
+ /// Empty for non-credit-card items.
+ ///
+ public string CardExpiryYear { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the credit card CVV.
+ /// Empty for non-credit-card items.
+ ///
+ public string CardCvv { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the credit card PIN.
+ /// Empty for non-credit-card items.
+ ///
+ public string CardPin { get; set; } = string.Empty;
+
///
/// Gets or sets the notes.
///