Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-10 14:11:33 -06:00
parent a903aa7283
commit af64f0a7d9
16 changed files with 104 additions and 209 deletions

View File

@ -5,7 +5,6 @@ import SwiftUI
@Model
final class BusinessCard {
var id: UUID
var displayName: String
var role: String
var company: String
var label: String
@ -46,7 +45,6 @@ final class BusinessCard {
init(
id: UUID = UUID(),
displayName: String = "",
role: String = "",
company: String = "",
label: String = "Work",
@ -74,7 +72,6 @@ final class BusinessCard {
logoData: Data? = nil
) {
self.id = id
self.displayName = displayName
self.role = role
self.company = company
self.label = label
@ -197,13 +194,12 @@ final class BusinessCard {
}
}
/// Computed display name with special formatting:
/// - Preferred name in quotes: "Bubba"
/// - Maiden name in parentheses: (Hackney)
/// - Pronouns in parentheses: (He/Him)
var computedDisplayName: String {
if !displayName.isEmpty { return displayName }
// MARK: - Name Properties (Single Source of Truth)
/// The full formatted name - THIS IS THE SINGLE SOURCE OF TRUTH for display.
/// Always computed from individual name fields.
/// Includes special formatting: preferred name in quotes, maiden name and pronouns in parentheses.
var fullName: String {
var parts: [String] = []
if !prefix.isEmpty { parts.append(prefix) }
@ -218,18 +214,12 @@ final class BusinessCard {
return parts.joined(separator: " ")
}
/// Returns the simple name for display (without formatting) - used for vCard
var simpleName: String {
if !displayName.isEmpty { return displayName }
let parts = [prefix, firstName, middleName, lastName, suffix].filter { !$0.isEmpty }
return parts.joined(separator: " ")
}
/// Returns the name to display (preferredName or first/last name)
var effectiveDisplayName: String {
if !preferredName.isEmpty { return preferredName }
let parts = [firstName, lastName].filter { !$0.isEmpty }
return parts.isEmpty ? computedDisplayName : parts.joined(separator: " ")
/// Plain name for vCard export (no quotes or parentheses formatting).
/// Used when generating contact cards for sharing.
var vCardName: String {
[prefix, firstName, middleName, lastName, suffix]
.filter { !$0.isEmpty }
.joined(separator: " ")
}
@MainActor
@ -246,7 +236,7 @@ final class BusinessCard {
lines.append("N:\(structuredName)")
// FN: Formatted name
let formattedName = simpleName.isEmpty ? displayName : simpleName
let formattedName = vCardName
lines.append("FN:\(escapeVCardValue(formattedName))")
// NICKNAME: Preferred name
@ -378,7 +368,7 @@ final class BusinessCard {
lines.append("N:\(structuredName)")
// FN: Formatted name
let formattedName = simpleName.isEmpty ? displayName : simpleName
let formattedName = vCardName
lines.append("FN:\(escapeVCardValue(formattedName))")
// PHOTO: Embedded profile photo as base64-encoded JPEG
@ -513,7 +503,6 @@ extension BusinessCard {
// Sample 1: Property Developer - Uses coverWithAvatarAndLogo layout
// Best when: Has cover, logo, and profile - logo centered on cover
let sample1 = BusinessCard(
displayName: "Daniel Sullivan",
role: "Property Developer",
company: "WR Construction",
label: "Work",
@ -522,6 +511,8 @@ extension BusinessCard {
layoutStyleRawValue: "split",
headerLayoutRawValue: "coverWithAvatarAndLogo",
avatarSystemName: "person.crop.circle",
firstName: "Daniel",
lastName: "Sullivan",
pronouns: "he/him",
bio: "Building the future of Dallas real estate"
)
@ -535,7 +526,6 @@ extension BusinessCard {
// Sample 2: Creative Lead - Uses logoBanner layout
// Best when: Strong logo, no cover needed
let sample2 = BusinessCard(
displayName: "Maya Chen",
role: "Creative Lead",
company: "Signal Studio",
label: "Creative",
@ -544,6 +534,8 @@ extension BusinessCard {
layoutStyleRawValue: "stacked",
headerLayoutRawValue: "logoBanner",
avatarSystemName: "sparkles",
firstName: "Maya",
lastName: "Chen",
pronouns: "she/her",
bio: "Designing experiences that matter"
)
@ -558,7 +550,6 @@ extension BusinessCard {
// Sample 3: DJ - Uses profileBanner layout
// Best when: Strong profile photo, personal brand
let sample3 = BusinessCard(
displayName: "DJ Michaels",
role: "DJ",
company: "Live Sessions",
label: "Music",
@ -567,6 +558,8 @@ extension BusinessCard {
layoutStyleRawValue: "photo",
headerLayoutRawValue: "profileBanner",
avatarSystemName: "music.mic",
firstName: "DJ",
lastName: "Michaels",
bio: "Bringing the beats to your events"
)
context.insert(sample3)

View File

@ -6,21 +6,21 @@ struct ShareLinkService: ShareLinkProviding {
}
func smsURL(for card: BusinessCard) -> URL {
let body = String.localized("ShareTextBody", card.displayName, card.shareURL.absoluteString)
let body = String.localized("ShareTextBody", card.fullName, card.shareURL.absoluteString)
let query = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
return URL(string: "sms:&body=\(query)") ?? card.shareURL
}
func emailURL(for card: BusinessCard) -> URL {
let subject = String.localized("ShareEmailSubject", card.displayName)
let body = String.localized("ShareEmailBody", card.displayName, card.shareURL.absoluteString)
let subject = String.localized("ShareEmailSubject", card.fullName)
let body = String.localized("ShareEmailBody", card.fullName, card.shareURL.absoluteString)
let subjectQuery = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
let bodyQuery = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
return URL(string: "mailto:?subject=\(subjectQuery)&body=\(bodyQuery)") ?? card.shareURL
}
func whatsappURL(for card: BusinessCard) -> URL {
let message = String.localized("ShareWhatsAppBody", card.displayName, card.shareURL.absoluteString)
let message = String.localized("ShareWhatsAppBody", card.fullName, card.shareURL.absoluteString)
let query = message.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
return URL(string: "https://wa.me/?text=\(query)") ?? card.shareURL
}

View File

@ -11,7 +11,7 @@ struct VCardFileService {
let payload = card.vCardFilePayload
// Create a sanitized filename from the card's name
let sanitizedName = sanitizeFilename(card.simpleName.isEmpty ? card.displayName : card.simpleName)
let sanitizedName = sanitizeFilename(card.vCardName)
let filename = sanitizedName.isEmpty ? "contact" : sanitizedName
// Write to temp directory with .vcf extension

View File

@ -131,9 +131,9 @@ final class WatchConnectivityService: NSObject {
let addressValue = card.firstContactField(ofType: "address")?.value ?? ""
let location = PostalAddress.decode(from: addressValue)?.singleLineString ?? addressValue
// Build vCard payload for QR code generation
// Build vCard payload for QR code generation (use vCardName for compatibility)
let vCardPayload = buildVCardPayload(
displayName: card.displayName,
displayName: card.vCardName,
company: card.company,
role: card.role,
phone: phone,
@ -149,9 +149,13 @@ final class WatchConnectivityService: NSObject {
// Generate QR code image data on iOS (CoreImage not available on watchOS)
let qrImageData = generateQRCodePNGData(from: vCardPayload)
// Use fullName - the single source of truth for display names
let syncDisplayName = card.fullName
Design.debugLog("WatchConnectivity: Syncing card '\(syncDisplayName)'")
return SyncableCard(
id: card.id,
displayName: card.displayName,
fullName: syncDisplayName,
role: card.role,
company: card.company,
email: email,
@ -269,7 +273,7 @@ extension WatchConnectivityService: WCSessionDelegate {
/// A simplified card structure that can be shared between iOS and watchOS
struct SyncableCard: Codable, Identifiable {
let id: UUID
var displayName: String
var fullName: String
var role: String
var company: String
var email: String

View File

@ -42,7 +42,7 @@ final class CardStore: BusinessCardProviding {
}
func updateCard(_ card: BusinessCard) {
Design.debugLog("CardStore: updateCard called for: \(card.displayName)")
Design.debugLog("CardStore: updateCard - fullName: '\(card.fullName)'")
card.updatedAt = .now
saveContext()
fetchCards()

View File

@ -28,7 +28,7 @@ struct BusinessCardView: View {
)
.accessibilityElement(children: .ignore)
.accessibilityLabel(String.localized("Business card"))
.accessibilityValue("\(card.effectiveDisplayName), \(card.role), \(card.company)")
.accessibilityValue("\(card.fullName), \(card.role), \(card.company)")
}
}
@ -172,7 +172,7 @@ private struct CardContentView: View {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
HStack(spacing: Design.Spacing.xSmall) {
Text(card.effectiveDisplayName)
Text(card.fullName)
.font(.title2)
.bold()
.foregroundStyle(textColor)
@ -390,11 +390,12 @@ private struct ContactFieldRowView: View {
#Preview("Profile Banner") {
@Previewable @State var card = BusinessCard(
displayName: "Matt Bruce",
role: "Lead iOS Developer",
company: "Toyota",
themeName: "Coral",
headerLayoutRawValue: "profileBanner"
headerLayoutRawValue: "profileBanner",
firstName: "Matt",
lastName: "Bruce"
)
BusinessCardView(card: card)
@ -404,11 +405,12 @@ private struct ContactFieldRowView: View {
#Preview("Cover + Avatar + Logo") {
@Previewable @State var card = BusinessCard(
displayName: "Matt Bruce",
role: "Lead iOS Developer",
company: "Toyota",
themeName: "Violet",
headerLayoutRawValue: "coverWithAvatarAndLogo"
headerLayoutRawValue: "coverWithAvatarAndLogo",
firstName: "Matt",
lastName: "Bruce"
)
BusinessCardView(card: card)

View File

@ -11,7 +11,6 @@ struct CardEditorView: View {
let onSave: (BusinessCard) -> Void
// Name fields
@State private var displayName = ""
@State private var prefix = ""
@State private var firstName = ""
@State private var middleName = ""
@ -86,20 +85,12 @@ struct CardEditorView: View {
private var isEditing: Bool { card != nil }
private var isFormValid: Bool {
!effectiveDisplayName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
!fullName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
/// Simple name for validation and storage (without quotes/parentheses formatting)
private var effectiveDisplayName: String {
if !displayName.isEmpty { return displayName }
let parts = [prefix, firstName, middleName, lastName, suffix].filter { !$0.isEmpty }
return parts.joined(separator: " ")
}
/// Formatted name for display with special formatting
private var formattedDisplayName: String {
if !displayName.isEmpty { return displayName }
/// The full formatted name - matches BusinessCard.fullName logic.
/// Single source of truth for name display in the editor.
private var fullName: String {
var parts: [String] = []
if !prefix.isEmpty { parts.append(prefix) }
@ -158,8 +149,8 @@ struct CardEditorView: View {
withAnimation { showNameDetails.toggle() }
} label: {
HStack {
Text(formattedDisplayName.isEmpty ? "Full Name" : formattedDisplayName)
.foregroundStyle(formattedDisplayName.isEmpty ? Color.secondary : Color.primary)
Text(fullName.isEmpty ? "Full Name" : fullName)
.foregroundStyle(fullName.isEmpty ? Color.secondary : Color.primary)
Spacer()
Image(systemName: showNameDetails ? "chevron.up" : "chevron.down")
.foregroundStyle(Color.accentColor)
@ -316,7 +307,7 @@ struct CardEditorView: View {
logoData: logoData,
avatarSystemName: avatarSystemName,
theme: selectedTheme,
displayName: effectiveDisplayName,
displayName: fullName,
role: role,
company: company
)
@ -1103,7 +1094,6 @@ private struct CardPreviewSheet: View {
private extension CardEditorView {
func loadCardData() {
guard let card else { return }
displayName = card.displayName
prefix = card.prefix
firstName = card.firstName
middleName = card.middleName
@ -1182,7 +1172,6 @@ private extension CardEditorView {
}
func updateCard(_ card: BusinessCard) {
card.displayName = displayName.isEmpty ? effectiveDisplayName : displayName
card.prefix = prefix
card.firstName = firstName
card.middleName = middleName
@ -1212,7 +1201,6 @@ private extension CardEditorView {
func createCard() -> BusinessCard {
let newCard = BusinessCard(
displayName: displayName.isEmpty ? effectiveDisplayName : displayName,
role: role,
company: company,
label: label,
@ -1246,7 +1234,6 @@ private extension CardEditorView {
func buildPreviewCard() -> BusinessCard {
let previewCard = BusinessCard(
displayName: displayName.isEmpty ? effectiveDisplayName : displayName,
role: role,
company: company,
label: label,

View File

@ -67,7 +67,7 @@ struct ShareCardView: View {
.ignoresSafeArea()
}
.sheet(item: $mailComposeURL) { url in
MailComposeView(vCardURL: url, cardName: appState.cardStore.selectedCard?.simpleName ?? "Contact")
MailComposeView(vCardURL: url, cardName: appState.cardStore.selectedCard?.vCardName ?? "Contact")
.ignoresSafeArea()
}
.onAppear {

View File

@ -53,7 +53,7 @@ private struct PhoneWidgetPreview: View {
.frame(width: Design.CardSize.widgetPhoneHeight, height: Design.CardSize.widgetPhoneHeight)
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text(card.displayName)
Text(card.fullName)
.font(.headline)
.foregroundStyle(Color.Text.primary)
Text(card.role)

View File

@ -95,8 +95,8 @@ struct BusinessCardTests {
let context = container.mainContext
// Insert cards directly instead of using samples (which might trigger other logic)
let card1 = BusinessCard(displayName: "Card One", role: "Role", company: "Company", isDefault: true)
let card2 = BusinessCard(displayName: "Card Two", role: "Role", company: "Company", isDefault: false)
let card1 = BusinessCard(role: "Role", company: "Company", isDefault: true, firstName: "Card", lastName: "One")
let card2 = BusinessCard(role: "Role", company: "Company", isDefault: false, firstName: "Card", lastName: "Two")
context.insert(card1)
context.insert(card2)
try context.save()
@ -143,9 +143,10 @@ struct BusinessCardTests {
let initialCount = store.cards.count
let newCard = BusinessCard(
displayName: "New User",
role: "Manager",
company: "New Corp"
company: "New Corp",
firstName: "New",
lastName: "User"
)
store.addCard(newCard)
@ -156,8 +157,8 @@ struct BusinessCardTests {
let container = try makeTestContainer()
let context = container.mainContext
let card1 = BusinessCard(displayName: "Card One", role: "Role", company: "Company")
let card2 = BusinessCard(displayName: "Card Two", role: "Role", company: "Company")
let card1 = BusinessCard(role: "Role", company: "Company", firstName: "Card", lastName: "One")
let card2 = BusinessCard(role: "Role", company: "Company", firstName: "Card", lastName: "Two")
context.insert(card1)
context.insert(card2)
try context.save()
@ -185,12 +186,13 @@ struct BusinessCardTests {
let store = CardStore(modelContext: context)
card.displayName = "Updated Name"
card.firstName = "Updated"
card.lastName = "Name"
card.role = "Updated Role"
store.updateCard(card)
let updatedCard = store.cards.first(where: { $0.id == card.id })
#expect(updatedCard?.displayName == "Updated Name")
#expect(updatedCard?.fullName == "Updated Name")
#expect(updatedCard?.role == "Updated Role")
}

View File

@ -9,6 +9,7 @@ enum WatchDesign {
}
enum Spacing {
static let extraSmall: CGFloat = 4
static let small: CGFloat = 6
static let medium: CGFloat = 10
static let large: CGFloat = 16

View File

@ -1,10 +1,10 @@
import Foundation
import SwiftUI
/// A simplified card structure synced from the iOS app via App Group UserDefaults
/// A simplified card structure synced from the iOS app via WatchConnectivity
struct WatchCard: Codable, Identifiable, Hashable {
let id: UUID
var displayName: String
var fullName: String
var role: String
var company: String
var email: String

View File

@ -14,6 +14,7 @@
}
},
"Choose default" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@ -36,6 +37,7 @@
}
},
"Default Card" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@ -57,15 +59,12 @@
}
}
},
"Default card QR code" : {
"comment" : "An accessibility label describing a view that shows the QR code of a user's default watch card.",
"isCommentAutoGenerated" : true
},
"No Cards" : {
"comment" : "A message displayed when a user has no watch cards.",
"isCommentAutoGenerated" : true
},
"Not selected" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@ -91,7 +90,12 @@
"comment" : "A description of the action to create a watch card from the iPhone app.",
"isCommentAutoGenerated" : true
},
"QR code" : {
"comment" : "A label describing a view that displays a QR code.",
"isCommentAutoGenerated" : true
},
"Selected" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {

View File

@ -49,7 +49,7 @@ final class WatchConnectivityService: NSObject {
let watchCards = syncableCards.map { syncable in
WatchCard(
id: syncable.id,
displayName: syncable.displayName,
fullName: syncable.fullName,
role: syncable.role,
company: syncable.company,
email: syncable.email,
@ -108,7 +108,7 @@ extension WatchConnectivityService: WCSessionDelegate {
/// Syncable card structure matching iOS side (for decoding)
private struct SyncableCard: Codable, Identifiable {
let id: UUID
var displayName: String
var fullName: String
var role: String
var company: String
var email: String

View File

@ -4,56 +4,26 @@ import Observation
@Observable
@MainActor
final class WatchCardStore {
private static let defaultCardIDKey = "WatchDefaultCardID"
private(set) var cards: [WatchCard] = []
var defaultCardID: UUID? {
didSet {
persistDefaultID()
}
}
/// The ID of the default card (synced from iPhone)
private(set) var defaultCardID: UUID?
init() {
loadDefaultID()
WatchDesign.debugLog("WatchCardStore: Initialized, waiting for cards from iPhone")
// Set up callback for when cards are received from iPhone
WatchConnectivityService.shared.onCardsReceived = { [weak self] cards in
self?.updateCards(cards)
}
#if targetEnvironment(simulator)
// Load sample data on simulator since WatchConnectivity often doesn't work
if cards.isEmpty {
loadSimulatorSampleData()
}
#endif
}
#if targetEnvironment(simulator)
private func loadSimulatorSampleData() {
WatchDesign.debugLog("WatchCardStore: Loading simulator sample data")
cards = [
WatchCard(
id: UUID(),
displayName: "Test User",
role: "iOS Developer",
company: "Sample Corp",
email: "test@example.com",
phone: "+1 555-0123",
website: "example.com",
location: "San Francisco, CA",
isDefault: true,
qrCodeImageData: nil
)
]
defaultCardID = cards.first?.id
}
#endif
/// The default card to display (always synced from iPhone)
var defaultCard: WatchCard? {
guard let defaultCardID else { return cards.first(where: { $0.isDefault }) ?? cards.first }
return cards.first(where: { $0.id == defaultCardID }) ?? cards.first(where: { $0.isDefault }) ?? cards.first
if let defaultCardID {
return cards.first { $0.id == defaultCardID }
}
return cards.first { $0.isDefault } ?? cards.first
}
/// Called by WatchConnectivityService when cards are received from iPhone
@ -61,26 +31,7 @@ final class WatchCardStore {
WatchDesign.debugLog("WatchCardStore: Received \(newCards.count) cards from iPhone")
cards = newCards
// Update default card ID if current selection is no longer valid
if let currentDefault = defaultCardID, !cards.contains(where: { $0.id == currentDefault }) {
defaultCardID = cards.first(where: { $0.isDefault })?.id ?? cards.first?.id
} else if defaultCardID == nil {
defaultCardID = cards.first(where: { $0.isDefault })?.id ?? cards.first?.id
}
}
func setDefault(_ card: WatchCard) {
defaultCardID = card.id
}
private func persistDefaultID() {
UserDefaults.standard.set(defaultCardID?.uuidString ?? "", forKey: Self.defaultCardIDKey)
}
private func loadDefaultID() {
let storedValue = UserDefaults.standard.string(forKey: Self.defaultCardIDKey) ?? ""
if let id = UUID(uuidString: storedValue) {
defaultCardID = id
}
// Always use the iPhone's default card - the watch no longer has its own picker
defaultCardID = cards.first { $0.isDefault }?.id ?? cards.first?.id
}
}

View File

@ -4,21 +4,11 @@ struct WatchContentView: View {
@Environment(WatchCardStore.self) private var cardStore
var body: some View {
ScrollView {
VStack(spacing: WatchDesign.Spacing.large) {
if let card = cardStore.defaultCard {
WatchQRCodeCardView(card: card)
} else if cardStore.cards.isEmpty {
WatchEmptyStateView()
}
if !cardStore.cards.isEmpty {
WatchCardPickerView()
}
}
.padding(WatchDesign.Spacing.medium)
if let card = cardStore.defaultCard {
WatchQRCodeCardView(card: card)
} else {
WatchEmptyStateView()
}
.background(Color.WatchPalette.background)
}
}
@ -46,11 +36,7 @@ private struct WatchQRCodeCardView: View {
let card: WatchCard
var body: some View {
VStack(spacing: WatchDesign.Spacing.small) {
Text("Default Card")
.font(.headline)
.foregroundStyle(Color.WatchPalette.text)
VStack(spacing: WatchDesign.Spacing.medium) {
if let image = card.qrCodeImage {
image
.resizable()
@ -71,61 +57,26 @@ private struct WatchQRCodeCardView: View {
.foregroundStyle(Color.WatchPalette.muted)
}
.frame(width: WatchDesign.Size.qrSize, height: WatchDesign.Size.qrSize)
.background(Color.WatchPalette.card)
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
.background(Color.WatchPalette.card)
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
}
Text(card.displayName)
.font(.subheadline)
.foregroundStyle(Color.WatchPalette.text)
Text(card.role)
.font(.caption)
.foregroundStyle(Color.WatchPalette.muted)
VStack(spacing: WatchDesign.Spacing.extraSmall) {
Text(card.fullName)
.font(.headline)
.foregroundStyle(Color.WatchPalette.text)
Text(card.role)
.font(.caption)
.foregroundStyle(Color.WatchPalette.muted)
}
}
.padding(WatchDesign.Spacing.medium)
.background(Color.WatchPalette.card)
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.WatchPalette.background)
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "Default card QR code"))
.accessibilityValue("\(card.displayName), \(card.role)")
}
}
private struct WatchCardPickerView: View {
@Environment(WatchCardStore.self) private var cardStore
var body: some View {
VStack(alignment: .leading, spacing: WatchDesign.Spacing.small) {
Text("Choose default")
.font(.headline)
.foregroundStyle(Color.WatchPalette.text)
ForEach(cardStore.cards) { card in
Button {
cardStore.setDefault(card)
} label: {
HStack {
Text(card.displayName)
.foregroundStyle(Color.WatchPalette.text)
Spacer()
if card.id == cardStore.defaultCardID {
Image(systemName: "checkmark")
.foregroundStyle(Color.WatchPalette.accent)
}
}
}
.buttonStyle(.plain)
.padding(.vertical, WatchDesign.Spacing.small)
.padding(.horizontal, WatchDesign.Spacing.medium)
.frame(maxWidth: .infinity, alignment: .leading)
.background(card.id == cardStore.defaultCardID ? Color.WatchPalette.accent.opacity(WatchDesign.Opacity.strong) : Color.WatchPalette.card)
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.medium))
.accessibilityValue(card.id == cardStore.defaultCardID ? String(localized: "Selected") : String(localized: "Not selected"))
}
}
.padding(WatchDesign.Spacing.medium)
.background(Color.WatchPalette.card.opacity(WatchDesign.Opacity.hint))
.clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large))
.accessibilityLabel(String(localized: "QR code"))
.accessibilityValue("\(card.fullName), \(card.role)")
}
}