From 3007805011a0141d4981be678e69ab4d49d4880a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 9 Jan 2026 16:09:01 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- BusinessCard.xcodeproj/project.pbxproj | 139 ++++++++++++++++-- .../xcschemes/xcschememanagement.plist | 9 +- BusinessCard/Services/WatchSyncService.swift | 95 +++++++++++- .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../BusinessCardWatch.entitlements | 0 .../BusinessCardWatchApp.swift | 0 .../Design/WatchDesignConstants.swift | 0 .../Models/WatchCard.swift | 31 ++-- .../Resources/Localizable.xcstrings | 3 +- .../State/WatchCardStore.swift | 0 .../Views/WatchContentView.swift | 21 ++- .../Services/WatchQRCodeService.swift | 17 --- ROADMAP.md | 51 +++++-- 14 files changed, 300 insertions(+), 66 deletions(-) rename {BusinessCardWatch => BusinessCardWatch Watch App}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {BusinessCardWatch => BusinessCardWatch Watch App}/Assets.xcassets/Contents.json (100%) rename {BusinessCardWatch => BusinessCardWatch Watch App}/BusinessCardWatch.entitlements (100%) rename {BusinessCardWatch => BusinessCardWatch Watch App}/BusinessCardWatchApp.swift (100%) rename {BusinessCardWatch => BusinessCardWatch Watch App}/Design/WatchDesignConstants.swift (100%) rename {BusinessCardWatch => BusinessCardWatch Watch App}/Models/WatchCard.swift (71%) rename {BusinessCardWatch => BusinessCardWatch Watch App}/Resources/Localizable.xcstrings (98%) rename {BusinessCardWatch => BusinessCardWatch Watch App}/State/WatchCardStore.swift (100%) rename {BusinessCardWatch => BusinessCardWatch Watch App}/Views/WatchContentView.swift (84%) delete mode 100644 BusinessCardWatch/Services/WatchQRCodeService.swift diff --git a/BusinessCard.xcodeproj/project.pbxproj b/BusinessCard.xcodeproj/project.pbxproj index 3e222ef..cc73836 100644 --- a/BusinessCard.xcodeproj/project.pbxproj +++ b/BusinessCard.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ EA8379232F105F2600077F87 /* BusinessCard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BusinessCard.app; sourceTree = BUILT_PRODUCTS_DIR; }; EA8379302F105F2800077F87 /* BusinessCardTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BusinessCardTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BusinessCardUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "BusinessCardWatch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -45,11 +46,6 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 05CFDAD65474442D8E3E309E /* BusinessCardWatch */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = BusinessCardWatch; - sourceTree = ""; - }; EA8379252F105F2600077F87 /* BusinessCard */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -68,6 +64,11 @@ path = BusinessCardUITests; sourceTree = ""; }; + EA837F992F11B16400077F87 /* BusinessCardWatch Watch App */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "BusinessCardWatch Watch App"; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -100,6 +101,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + EA837F952F11B16400077F87 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -107,9 +115,9 @@ isa = PBXGroup; children = ( EA8379252F105F2600077F87 /* BusinessCard */, - 05CFDAD65474442D8E3E309E /* BusinessCardWatch */, EA8379332F105F2800077F87 /* BusinessCardTests */, EA83793D2F105F2800077F87 /* BusinessCardUITests */, + EA837F992F11B16400077F87 /* BusinessCardWatch Watch App */, EA8379242F105F2600077F87 /* Products */, ); sourceTree = ""; @@ -121,6 +129,7 @@ 60186E73BC8040538616865B /* BusinessCardWatch.app */, EA8379302F105F2800077F87 /* BusinessCardTests.xctest */, EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */, + EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */, ); name = Products; sourceTree = ""; @@ -140,9 +149,6 @@ ); dependencies = ( ); - fileSystemSynchronizedGroups = ( - 05CFDAD65474442D8E3E309E /* BusinessCardWatch */, - ); name = BusinessCardWatch; packageProductDependencies = ( ); @@ -219,6 +225,28 @@ productReference = EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + EA837F972F11B16400077F87 /* BusinessCardWatch Watch App */ = { + isa = PBXNativeTarget; + buildConfigurationList = EA837FA02F11B16400077F87 /* Build configuration list for PBXNativeTarget "BusinessCardWatch Watch App" */; + buildPhases = ( + EA837F942F11B16400077F87 /* Sources */, + EA837F952F11B16400077F87 /* Frameworks */, + EA837F962F11B16400077F87 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + EA837F992F11B16400077F87 /* BusinessCardWatch Watch App */, + ); + name = "BusinessCardWatch Watch App"; + packageProductDependencies = ( + ); + productName = "BusinessCardWatch Watch App"; + productReference = EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -243,6 +271,9 @@ CreatedOnToolsVersion = 26.0; TestTargetID = EA8379222F105F2600077F87; }; + EA837F972F11B16400077F87 = { + CreatedOnToolsVersion = 26.0; + }; }; }; buildConfigurationList = EA83791E2F105F2600077F87 /* Build configuration list for PBXProject "BusinessCard" */; @@ -268,6 +299,7 @@ D007169724A44109B518B9E6 /* BusinessCardWatch */, EA83792F2F105F2800077F87 /* BusinessCardTests */, EA8379392F105F2800077F87 /* BusinessCardUITests */, + EA837F972F11B16400077F87 /* BusinessCardWatch Watch App */, ); }; /* End PBXProject section */ @@ -301,6 +333,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + EA837F962F11B16400077F87 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -332,6 +371,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + EA837F942F11B16400077F87 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -675,6 +721,72 @@ }; name = Release; }; + EA837FA12F11B16400077F87 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6R7KLBPBLZ; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = .watchkitapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 26.0; + }; + name = Debug; + }; + EA837FA22F11B16400077F87 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6R7KLBPBLZ; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = BusinessCardWatch; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = .watchkitapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 26.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -723,6 +835,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + EA837FA02F11B16400077F87 /* Build configuration list for PBXNativeTarget "BusinessCardWatch Watch App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EA837FA12F11B16400077F87 /* Debug */, + EA837FA22F11B16400077F87 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ diff --git a/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist index a47ee2b..b34a487 100644 --- a/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/BusinessCard.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,17 @@ BusinessCard.xcscheme_^#shared#^_ orderHint - 2 + 1 + + BusinessCardWatch Watch App.xcscheme_^#shared#^_ + + orderHint + 3 BusinessCardWatch.xcscheme_^#shared#^_ orderHint - 1 + 2 diff --git a/BusinessCard/Services/WatchSyncService.swift b/BusinessCard/Services/WatchSyncService.swift index 30aebd5..90b8c85 100644 --- a/BusinessCard/Services/WatchSyncService.swift +++ b/BusinessCard/Services/WatchSyncService.swift @@ -1,4 +1,7 @@ import Foundation +import CoreImage +import CoreImage.CIFilterBuiltins +import UIKit /// Syncs card data to watchOS via shared App Group UserDefaults struct WatchSyncService { @@ -27,6 +30,24 @@ struct WatchSyncService { let addressValue = card.firstContactField(ofType: "address")?.value ?? "" let location = PostalAddress.decode(from: addressValue)?.singleLineString ?? addressValue + // Build vCard payload for QR code generation + let vCardPayload = buildVCardPayload( + displayName: card.displayName, + company: card.company, + role: card.role, + phone: phone, + email: email, + website: website, + location: location, + bio: card.bio, + linkedIn: linkedIn, + twitter: twitter, + instagram: instagram + ) + + // Generate QR code image data on iOS (CoreImage not available on watchOS) + let qrImageData = generateQRCodePNGData(from: vCardPayload) + return SyncableCard( id: card.id, displayName: card.displayName, @@ -41,7 +62,8 @@ struct WatchSyncService { bio: card.bio, linkedIn: linkedIn, twitter: twitter, - instagram: instagram + instagram: instagram, + qrCodeImageData: qrImageData ) } @@ -49,6 +71,75 @@ struct WatchSyncService { defaults.set(encoded, forKey: cardsKey) } } + + private static func buildVCardPayload( + displayName: String, + company: String, + role: String, + phone: String, + email: String, + website: String, + location: String, + bio: String, + linkedIn: String, + twitter: String, + instagram: String + ) -> String { + var lines = [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:\(displayName)", + "ORG:\(company)", + "TITLE:\(role)" + ] + + if !phone.isEmpty { + lines.append("TEL;TYPE=work:\(phone)") + } + if !email.isEmpty { + lines.append("EMAIL;TYPE=work:\(email)") + } + if !website.isEmpty { + lines.append("URL:\(website)") + } + if !location.isEmpty { + lines.append("ADR;TYPE=work:;;\(location)") + } + if !bio.isEmpty { + lines.append("NOTE:\(bio)") + } + if !linkedIn.isEmpty { + lines.append("X-SOCIALPROFILE;TYPE=linkedin:\(linkedIn)") + } + if !twitter.isEmpty { + lines.append("X-SOCIALPROFILE;TYPE=twitter:\(twitter)") + } + if !instagram.isEmpty { + lines.append("X-SOCIALPROFILE;TYPE=instagram:\(instagram)") + } + + lines.append("END:VCARD") + return lines.joined(separator: "\n") + } + + private static func generateQRCodePNGData(from payload: String) -> Data? { + let context = CIContext() + let data = Data(payload.utf8) + let filter = CIFilter.qrCodeGenerator() + filter.setValue(data, forKey: "inputMessage") + filter.correctionLevel = "M" + + guard let outputImage = filter.outputImage else { return nil } + + // Scale up for better quality on watch display + let scaledImage = outputImage.transformed(by: CGAffineTransform(scaleX: 10, y: 10)) + + guard let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) else { return nil } + + // Convert to PNG data for syncing + let uiImage = UIImage(cgImage: cgImage) + return uiImage.pngData() + } } /// A simplified card structure that can be shared between iOS and watchOS @@ -67,6 +158,8 @@ struct SyncableCard: Codable, Identifiable { var linkedIn: String var twitter: String var instagram: String + /// Pre-generated QR code PNG data (CoreImage not available on watchOS) + var qrCodeImageData: Data? var vCardPayload: String { var lines = [ diff --git a/BusinessCardWatch/Assets.xcassets/AppIcon.appiconset/Contents.json b/BusinessCardWatch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from BusinessCardWatch/Assets.xcassets/AppIcon.appiconset/Contents.json rename to BusinessCardWatch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/BusinessCardWatch/Assets.xcassets/Contents.json b/BusinessCardWatch Watch App/Assets.xcassets/Contents.json similarity index 100% rename from BusinessCardWatch/Assets.xcassets/Contents.json rename to BusinessCardWatch Watch App/Assets.xcassets/Contents.json diff --git a/BusinessCardWatch/BusinessCardWatch.entitlements b/BusinessCardWatch Watch App/BusinessCardWatch.entitlements similarity index 100% rename from BusinessCardWatch/BusinessCardWatch.entitlements rename to BusinessCardWatch Watch App/BusinessCardWatch.entitlements diff --git a/BusinessCardWatch/BusinessCardWatchApp.swift b/BusinessCardWatch Watch App/BusinessCardWatchApp.swift similarity index 100% rename from BusinessCardWatch/BusinessCardWatchApp.swift rename to BusinessCardWatch Watch App/BusinessCardWatchApp.swift diff --git a/BusinessCardWatch/Design/WatchDesignConstants.swift b/BusinessCardWatch Watch App/Design/WatchDesignConstants.swift similarity index 100% rename from BusinessCardWatch/Design/WatchDesignConstants.swift rename to BusinessCardWatch Watch App/Design/WatchDesignConstants.swift diff --git a/BusinessCardWatch/Models/WatchCard.swift b/BusinessCardWatch Watch App/Models/WatchCard.swift similarity index 71% rename from BusinessCardWatch/Models/WatchCard.swift rename to BusinessCardWatch Watch App/Models/WatchCard.swift index 749bb1a..59848cc 100644 --- a/BusinessCardWatch/Models/WatchCard.swift +++ b/BusinessCardWatch Watch App/Models/WatchCard.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftUI /// A simplified card structure synced from the iOS app via App Group UserDefaults struct WatchCard: Codable, Identifiable, Hashable { @@ -11,21 +12,14 @@ struct WatchCard: Codable, Identifiable, Hashable { var website: String var location: String var isDefault: Bool + /// Pre-generated QR code PNG data from iOS (CoreImage not available on watchOS) + var qrCodeImageData: Data? - var vCardPayload: String { - let lines = [ - "BEGIN:VCARD", - "VERSION:3.0", - "FN:\(displayName)", - "ORG:\(company)", - "TITLE:\(role)", - "TEL;TYPE=work:\(phone)", - "EMAIL;TYPE=work:\(email)", - "URL:\(website)", - "ADR;TYPE=work:;;\(location)", - "END:VCARD" - ] - return lines.joined(separator: "\n") + /// Returns a SwiftUI Image from the synced QR code data + var qrCodeImage: Image? { + guard let data = qrCodeImageData, + let uiImage = UIImage(data: data) else { return nil } + return Image(uiImage: uiImage) } } @@ -40,7 +34,8 @@ extension WatchCard { phone: "+1 (214) 987-7810", website: "wrconstruction.co", location: "Dallas, TX", - isDefault: true + isDefault: true, + qrCodeImageData: nil ), WatchCard( id: UUID(), @@ -51,7 +46,8 @@ extension WatchCard { phone: "+1 (312) 404-2211", website: "signal.studio", location: "Chicago, IL", - isDefault: false + isDefault: false, + qrCodeImageData: nil ), WatchCard( id: UUID(), @@ -62,7 +58,8 @@ extension WatchCard { phone: "+1 (646) 222-3300", website: "livesessions.fm", location: "New York, NY", - isDefault: false + isDefault: false, + qrCodeImageData: nil ) ] } diff --git a/BusinessCardWatch/Resources/Localizable.xcstrings b/BusinessCardWatch Watch App/Resources/Localizable.xcstrings similarity index 98% rename from BusinessCardWatch/Resources/Localizable.xcstrings rename to BusinessCardWatch Watch App/Resources/Localizable.xcstrings index 473bffb..a36b418 100644 --- a/BusinessCardWatch/Resources/Localizable.xcstrings +++ b/BusinessCardWatch Watch App/Resources/Localizable.xcstrings @@ -29,5 +29,6 @@ "fr-CA" : { "stringUnit" : { "state" : "translated", "value" : "Sélectionnée" } } } } - } + }, + "version" : "1.1" } diff --git a/BusinessCardWatch/State/WatchCardStore.swift b/BusinessCardWatch Watch App/State/WatchCardStore.swift similarity index 100% rename from BusinessCardWatch/State/WatchCardStore.swift rename to BusinessCardWatch Watch App/State/WatchCardStore.swift diff --git a/BusinessCardWatch/Views/WatchContentView.swift b/BusinessCardWatch Watch App/Views/WatchContentView.swift similarity index 84% rename from BusinessCardWatch/Views/WatchContentView.swift rename to BusinessCardWatch Watch App/Views/WatchContentView.swift index e2a7aac..cd5b782 100644 --- a/BusinessCardWatch/Views/WatchContentView.swift +++ b/BusinessCardWatch Watch App/Views/WatchContentView.swift @@ -2,13 +2,12 @@ import SwiftUI struct WatchContentView: View { @Environment(WatchCardStore.self) private var cardStore - private let qrService = WatchQRCodeService() var body: some View { ScrollView { VStack(spacing: WatchDesign.Spacing.large) { if let card = cardStore.defaultCard { - WatchQRCodeCardView(card: card, qrService: qrService) + WatchQRCodeCardView(card: card) } else if cardStore.cards.isEmpty { WatchEmptyStateView() } @@ -48,7 +47,6 @@ private struct WatchEmptyStateView: View { private struct WatchQRCodeCardView: View { let card: WatchCard - let qrService: WatchQRCodeService var body: some View { VStack(spacing: WatchDesign.Spacing.small) { @@ -56,8 +54,8 @@ private struct WatchQRCodeCardView: View { .font(.headline) .foregroundStyle(Color.WatchPalette.text) - if let image = qrService.qrCode(from: card.vCardPayload) { - Image(decorative: image, scale: 1) + if let image = card.qrCodeImage { + image .resizable() .interpolation(.none) .scaledToFit() @@ -65,6 +63,19 @@ private struct WatchQRCodeCardView: View { .padding(WatchDesign.Spacing.small) .background(Color.WatchPalette.card) .clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large)) + } else { + // Fallback when no QR code synced yet + VStack(spacing: WatchDesign.Spacing.small) { + Image(systemName: "qrcode") + .font(.largeTitle) + .foregroundStyle(Color.WatchPalette.muted) + Text("Sync from iPhone") + .font(.caption2) + .foregroundStyle(Color.WatchPalette.muted) + } + .frame(width: WatchDesign.Size.qrSize, height: WatchDesign.Size.qrSize) + .background(Color.WatchPalette.card) + .clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.large)) } Text(card.displayName) diff --git a/BusinessCardWatch/Services/WatchQRCodeService.swift b/BusinessCardWatch/Services/WatchQRCodeService.swift deleted file mode 100644 index a3779c0..0000000 --- a/BusinessCardWatch/Services/WatchQRCodeService.swift +++ /dev/null @@ -1,17 +0,0 @@ -import CoreImage -import CoreImage.CIFilterBuiltins -import CoreGraphics - -struct WatchQRCodeService { - private let context = CIContext() - - func qrCode(from payload: String) -> CGImage? { - let data = Data(payload.utf8) - let filter = CIFilter.qrCodeGenerator() - filter.setValue(data, forKey: "inputMessage") - filter.correctionLevel = "M" - guard let outputImage = filter.outputImage else { return nil } - let scaledImage = outputImage.transformed(by: CGAffineTransform(scaleX: 10, y: 10)) - return context.createCGImage(scaledImage, from: scaledImage.extent) - } -} diff --git a/ROADMAP.md b/ROADMAP.md index 5e2c91b..652b003 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -67,22 +67,40 @@ This document tracks planned features and their implementation status. ## 🔲 Planned Features -### Medium Priority (Differentiation) +### High Priority (Photo Sharing via App Clip) -- [ ] **Email signature export** - Generate HTML signature - - Generate professional HTML email signature from card data - - Copy to clipboard or share - - Multiple signature styles/templates +- [ ] **App Clip for instant card sharing** - Recipients get full card with photo, no app install + - Solves the QR code size limitation for photos + - Recipient scans QR → App Clip loads instantly → "Add to Contacts" with photo + - Uses CloudKit for ephemeral card storage (auto-deletes after save) -- [ ] **Card analytics** - View/scan counts - - Would require backend infrastructure - - Track when cards are viewed/scanned - - Show analytics dashboard + **Implementation phases:** -- [ ] **Virtual meeting background** - Generate image with QR - - Create background image with card info + QR code - - Export for Zoom, Teams, etc. - - Multiple background styles + 1. **CloudKit Setup** + - [ ] Enable CloudKit capability + - [ ] Create `SharedCard` record type (name, role, company, vCard data, photo asset) + - [ ] Add `expiresAt` field for auto-cleanup + - [ ] Implement upload function in main app + - [ ] Implement cleanup (delete expired cards on app launch) + + 2. **App Clip Target** + - [ ] Create App Clip target (<15MB) + - [ ] Configure Associated Domains (`appclips:yourapp.com`) + - [ ] Build minimal UI: card preview + "Add to Contacts" button + - [ ] Fetch card from CloudKit using URL parameter + - [ ] Use `CNContactStore` to save contact with photo + - [ ] Call delete/mark-complete on CloudKit after save + + 3. **Main App Integration** + - [ ] New "Share via App Clip" option in ShareCardView + - [ ] Upload card to CloudKit on share + - [ ] Generate QR code with App Clip URL + - [ ] Show QR code for scanning + + 4. **App Store Configuration** + - [ ] Configure App Clip experience in App Store Connect + - [ ] Set up App Clip Code or Smart App Banner (optional) + - [ ] Test invocation URLs ### Lower Priority (Advanced) @@ -100,6 +118,11 @@ This document tracks planned features and their implementation status. - Requires user accounts - Requires backend infrastructure - Team branding, shared templates + +- [ ] **Track share recipients** - Record who you shared your card with + - Add contact entry when sharing + - Track share date, method, and recipient info + - View share history per card --- @@ -134,4 +157,4 @@ This document tracks planned features and their implementation status. --- -*Last updated: January 8, 2026* +*Last updated: January 9, 2026*