Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
a903aa7283
commit
af64f0a7d9
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" : {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user