diff --git a/Agents.md b/Agents.md index d61e80e..e6b7ca7 100644 --- a/Agents.md +++ b/Agents.md @@ -419,6 +419,42 @@ If you need different formats for different purposes: - Name constants semantically: `accent` not `pointSix`, `large` not `sixteen`. +## App Identifiers + +**Centralize all company-specific identifiers** in a single configuration file for easy migration. + +### Structure + +Create `Configuration/AppIdentifiers.swift`: + +```swift +enum AppIdentifiers { + // MARK: - Company Identifier (CHANGE THIS FOR MIGRATION) + static let companyIdentifier = "com.yourcompany" + + // MARK: - Derived Identifiers + static var bundleIdentifier: String { "\(companyIdentifier).AppName" } + static var watchBundleIdentifier: String { "\(bundleIdentifier).watchkitapp" } + static var appClipBundleIdentifier: String { "\(bundleIdentifier).Clip" } + static var appGroupIdentifier: String { "group.\(companyIdentifier).AppName" } + static var cloudKitContainerIdentifier: String { "iCloud.\(companyIdentifier).AppName" } +} +``` + +### Usage + +- Always use `AppIdentifiers.*` instead of hardcoding bundle IDs or container names. +- `AppIdentifiers.companyIdentifier` is the single source of truth. +- All other identifiers derive from it automatically. + +### Migration + +When migrating to a new developer account: +1. Change `companyIdentifier` in `AppIdentifiers.swift` +2. Update entitlements files manually (cannot be tokenized) +3. Update bundle IDs in Xcode project settings + + ## Dynamic Type Instructions - Always support Dynamic Type for accessibility. diff --git a/BusinessCard/BusinessCardApp.swift b/BusinessCard/BusinessCardApp.swift index 6e1f29e..1d31d8c 100644 --- a/BusinessCard/BusinessCardApp.swift +++ b/BusinessCard/BusinessCardApp.swift @@ -15,7 +15,7 @@ struct BusinessCardApp: App { var container: ModelContainer? if let appGroupURL = FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: "group.com.mbrucedogs.BusinessCard" + forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier ) { let storeURL = appGroupURL.appending(path: "BusinessCard.store") let config = ModelConfiguration( diff --git a/BusinessCard/Configuration/AppIdentifiers.swift b/BusinessCard/Configuration/AppIdentifiers.swift new file mode 100644 index 0000000..f0a5463 --- /dev/null +++ b/BusinessCard/Configuration/AppIdentifiers.swift @@ -0,0 +1,32 @@ +import Foundation + +/// Centralized app identifiers for easy migration between developer accounts. +/// +/// When migrating to a new developer account: +/// 1. Update `companyIdentifier` below +/// 2. Update entitlements files manually (cannot be tokenized) +/// 3. Update bundle IDs in Xcode project settings +/// 4. See DevAccount-Migration.md for full checklist +enum AppIdentifiers { + + // MARK: - Company Identifier (CHANGE THIS FOR MIGRATION) + + /// The company's reverse domain identifier. + static let companyIdentifier = "com.mbrucedogs" + + // MARK: - Derived Identifiers + + static var bundleIdentifier: String { "\(companyIdentifier).BusinessCard" } + static var watchBundleIdentifier: String { "\(bundleIdentifier).watchkitapp" } + static var appClipBundleIdentifier: String { "\(bundleIdentifier).Clip" } + static var appGroupIdentifier: String { "group.\(companyIdentifier).BusinessCard" } + static var cloudKitContainerIdentifier: String { "iCloud.\(companyIdentifier).BusinessCard" } + + // MARK: - App Clip Configuration + + static let appClipDomain = "cards.example.com" + + static func appClipURL(recordName: String) -> URL? { + URL(string: "https://\(appClipDomain)/appclip?id=\(recordName)") + } +} diff --git a/CODEX-Plan-AppClip.md b/CODEX-Plan-AppClip.md deleted file mode 100644 index 536e388..0000000 --- a/CODEX-Plan-AppClip.md +++ /dev/null @@ -1,394 +0,0 @@ -# CODEX Plan - App Clip - -## Goal -Implement App Clip-based sharing so recipients can scan a QR code, open an App Clip, preview a card (including photo), and add it to Contacts without installing the full app. - -## Key Requirement (Photo Preservation) -- Use **vCard 3.0** with embedded photo data for contact creation. -- Current model already supports this via `BusinessCard.vCardFilePayload`: - - Includes `VERSION:3.0`. - - Embeds `PHOTO;ENCODING=b;TYPE=JPEG:`. -- App Clip should fetch the record, build a contact with **photo data** attached, and then save to Contacts. - -## Data Flow (End-to-End) -1. User taps **Share via App Clip** in iOS app. -2. iOS app creates a `SharedCard` CloudKit record: - - Stores vCard data (including PHOTO base64) + optional photo asset. - - Sets `expiresAt`. -3. iOS app generates an App Clip URL and a QR code from that URL. -4. Recipient scans QR code → App Clip opens with `recordID` in URL. -5. App Clip fetches `SharedCard` record from CloudKit. -6. App Clip builds a contact from the vCard + photo, shows preview. -7. User taps **Add to Contacts** → contact saved with photo. -8. App Clip marks record as consumed or deletes it (optional). - -## UX Expectations -- App Clip must load quickly and feel instant. -- Preview shows name, role, company, and profile photo. -- Primary action is always **Add to Contacts**. -- Success state shows confirmation and optionally offers “Open full app”. -- Errors show a brief, user-friendly message with retry. - -## CloudKit Record Design -Record type: `SharedCard` -- `recordID` (system) -- `fullName` (String) -- `role` (String) -- `company` (String) -- `vCardData` (String) // vCard 3.0 with PHOTO base64 -- `photoData` (CKAsset, optional) // optional raw photo file for preview -- `createdAt` (Date) -- `expiresAt` (Date) -- `consumedAt` (Date, optional) // set after App Clip saves contact - -**Notes:** -- Keep TTL short (e.g., 7 days) for privacy. -- Use CKAsset only for preview, vCard remains primary source of truth. - -## URL Format -- `https://yourapp.com/appclip?card=` -- Record ID must be safe for URL; use CKRecord.ID recordName. -- QR code payload should be only the URL (no vCard data). - -## Security / Privacy -- Short-lived records; delete expired on app launch and optionally after consumption. -- Keep payload minimal: avoid storing unnecessary data. -- Consider hashing record IDs in public URLs if desired (optional). -- No analytics or tracking unless explicitly approved. - -## Constraints -- iOS 26+, Swift 6.2, SwiftUI + `@Observable`. -- Protocol-first, Clean Architecture, one public type per file. -- String Catalogs only; no inline literals in views; use Design constants. -- No UIKit unless required (App Clip will need `CNContactStore`). -- WatchConnectivity remains unchanged. - -## Milestone 1: CloudKit Schema + Core Services (Main App) -- Add `SharedCard` CloudKit record design: - - Fields: `fullName`, `role`, `company`, `vCardData`, `photoData`, `expiresAt`, `createdAt`. - - Store `photoData` as `CKAsset` for size efficiency. -- Create protocols: - - `SharedCardUploading` (upload + return URL/record ID). - - `SharedCardFetching` (fetch by record ID). - - `SharedCardCleaning` (delete expired records). -- Implement a CloudKit service: - - `SharedCardCloudKitService` conforming to the protocols. - - Use async/await; no force-try. -- Add state integration: - - `ShareCardState` (or extend existing share state) to call upload and expose App Clip URL. - - Persist a short-lived “last shared” URL for display. -- Add cleanup hook on app launch: - - Trigger `cleanupExpiredSharedCards()` in `AppState` init or a dedicated startup service. - - Cleanup should be safe when offline (no crash, no force-try). - -### CloudKit Behavior Details -- Use the private database (not public) unless sharing requires public discovery. -- Upload is write-once for a given share, no edits required. -- `expiresAt` should be server-trusted if possible; otherwise use client time. -- Cleanup query: `expiresAt < now` and optionally `consumedAt != nil`. -- Prefer batching deletes to avoid rate limits. - -### Code Skeletons (Milestone 1) - -```swift -// BusinessCard/Protocols/SharedCardUploading.swift -protocol SharedCardUploading { - func uploadSharedCard(_ card: BusinessCard) async throws -> SharedCardUploadResult -} -``` - -```swift -// BusinessCard/Protocols/SharedCardFetching.swift -protocol SharedCardFetching { - func fetchSharedCard(recordID: String) async throws -> SharedCardPayload -} -``` - -```swift -// BusinessCard/Protocols/SharedCardCleaning.swift -protocol SharedCardCleaning { - func cleanupExpiredSharedCards() async throws -} -``` - -```swift -// BusinessCard/Models/SharedCardPayload.swift -struct SharedCardPayload: Sendable { - let recordID: String - let vCardData: String - let photoData: Data? - let expiresAt: Date -} -``` - -```swift -// BusinessCard/Models/SharedCardUploadResult.swift -struct SharedCardUploadResult: Sendable { - let recordID: String - let appClipURL: URL - let expiresAt: Date -} -``` - -```swift -// BusinessCard/Services/SharedCardCloudKitService.swift -struct SharedCardCloudKitService: SharedCardUploading, SharedCardFetching, SharedCardCleaning { - func uploadSharedCard(_ card: BusinessCard) async throws -> SharedCardUploadResult { - // Build vCard data using card.vCardFilePayload (includes photo base64). - // Upload record + asset, return App Clip URL. - } - - func fetchSharedCard(recordID: String) async throws -> SharedCardPayload { - // Fetch vCard string + photo asset for App Clip. - } - - func cleanupExpiredSharedCards() async throws { - // Query by expiresAt < now, delete records. - } -} -``` - -## App Clip Entitlements and Capabilities -- App Clip target must include: - - CloudKit capability - - Contacts access usage description - - Associated Domains - -## Milestone 2: Main App UI Integration -- Add “Share via App Clip” action in `ShareCardView`. -- Show a new QR code using App Clip URL (not the vCard URL). -- Add status UI for upload progress and errors. -- Ensure new strings are in `.xcstrings`. -- Add user-facing helper copy: - - “This QR code opens an App Clip with your full card and photo.” - - “Recipient can save the full contact without installing the app.” - -## Milestone 2.1: App Clip QR UI Details -- Use a dedicated QR view when App Clip URL is available. -- Provide a fallback state if upload fails (hide QR + show error). -- Show a lightweight loading state while upload is running. - -### Code Skeletons (Milestone 2) - -```swift -// BusinessCard/State/ShareCardState.swift -@MainActor -@Observable -final class ShareCardState { - private let uploader: SharedCardUploading - - var isUploading = false - var appClipURL: URL? - var lastUploadError: String? - - init(uploader: SharedCardUploading) { - self.uploader = uploader - } - - func shareViaAppClip(card: BusinessCard) async { - // Call uploader.uploadSharedCard(card) and set appClipURL. - } -} -``` - -### Suggested ShareCardState Flow -- Guard that a card exists before upload. -- Set `isUploading = true` before calling CloudKit. -- On success: set `appClipURL`, clear `lastUploadError`. -- On failure: set `lastUploadError`, keep `appClipURL` nil. -- Always reset `isUploading` in `defer`. - -## Milestone 3: App Clip Target -- Create App Clip target (<15 MB). -- Add minimal model type: - - `SharedCardSnapshot` (name, role, company, photo, vCardData). -- Add state: - - `AppClipCardStore` (`@Observable`, `@MainActor`). -- Add services: - - `AppClipCloudKitService` (fetch by record ID). -- Build UI: - - `AppClipRootView` with card preview. - - “Add to Contacts” primary action. -- Implement `CNContactStore` save flow: - - Map to `CNMutableContact`. - - Respect photo data and vCard fields. - -### Code Skeletons (Milestone 3) - -```swift -// BusinessCardAppClip/Models/SharedCardSnapshot.swift -struct SharedCardSnapshot: Sendable { - let vCardData: String - let photoData: Data? -} -``` - -```swift -// BusinessCardAppClip/Services/AppClipCloudKitService.swift -struct AppClipCloudKitService { - func fetchSharedCard(recordID: String) async throws -> SharedCardSnapshot { - // Fetch vCard + photo asset. - } -} -``` - -```swift -// BusinessCardAppClip/State/AppClipCardStore.swift -@MainActor -@Observable -final class AppClipCardStore { - private let service: AppClipCloudKitService - var snapshot: SharedCardSnapshot? - var isLoading = false - var errorMessage: String? - - init(service: AppClipCloudKitService) { - self.service = service - } - - func load(recordID: String) async { - // Fetch snapshot. - } - - func saveToContacts() async throws { - // Parse vCard data + photo and write with CNContactStore. - } -} -``` - -```swift -// BusinessCardAppClip/Views/AppClipRootView.swift -struct AppClipRootView: View { - @Bindable var store: AppClipCardStore - - var body: some View { - // Minimal preview + "Add to Contacts" button. - } -} -``` - -### Contact Save Implementation Sketch -```swift -// BusinessCardAppClip/Services/ContactSaveService.swift -struct ContactSaveService { - func save(vCardData: String, photoData: Data?) async throws { - // 1. Convert vCard string to Data. - // 2. Parse using CNContactVCardSerialization. - // 3. Create CNMutableContact, attach imageData if needed. - // 4. Save via CNContactStore. - } -} -``` - -## App Clip Contact Save Details -- Parse vCard data into `CNContact` using `CNContactVCardSerialization`. -- If vCard photo is missing or invalid, use `photoData` asset as fallback. -- Do not create duplicates if a similar contact already exists (optional, future). -- Provide success confirmation and a short success message. - -## App Clip Permissions -- Request Contacts access on first save attempt. -- If denied, show an explanatory message with Settings guidance. - -## Error States -- CloudKit fetch fails → show retry + short description. -- Record expired → show “This card has expired” and provide fallback. -- Contact save denied → show “Contacts access required” with instructions. -- Malformed vCard → show “This card data is invalid” (log details). - -## Milestone 4: App Clip Invocation + App Store Connect -- Configure Associated Domains: `appclips:yourapp.com`. -- Generate App Clip URL format: `https://yourapp.com/appclip?card=`. -- Configure App Clip Experience in App Store Connect. -- Validate invocation with QR code scans. - -## App Clip Size and Performance Notes -- Keep App Clip under 15 MB. -- Avoid loading unused assets. -- Use lightweight preview UI; no heavy animations. - -## Implementation Notes -- Use `String(localized:)` for non-`Text` strings in App Clip. -- Keep all user-facing text in the App Clip string catalog. -- Use Bedrock `Design` constants for spacing, corner radius, opacity, and animation. -- Avoid `GeometryReader`; use `containerRelativeFrame()` when possible. -- Use `.scrollIndicators(.hidden)` if any ScrollView is used. -- Ensure all interactive elements have accessibility labels/hints. - -## vCard Handling Notes -- vCard payload should remain ASCII-safe; base64 PHOTO may be long. -- vCard should be stored exactly as generated by `vCardFilePayload`. -- If App Clip uses `CNContactVCardSerialization`, preserve any existing fields. -- The App Clip should not attempt to parse fields manually unless needed for preview. - -## CloudKit Limits and Resilience -- CKRecord size limits apply; keep vCard payload small where possible. -- Photo asset should be a compressed JPEG to reduce size. -- If upload fails, allow retry and show a clear error state. -- If fetch fails due to network, show offline messaging and retry. - -## Testing Strategy -- Unit tests for CloudKit upload URL creation and expiration logic. -- Unit test for App Clip contact mapping (vCard + photo). -- Manual tests on device for App Clip invocation and contact save. - -## Open Questions (Resolve Before Build) -- Final App Clip domain and URL path? -- TTL for shared cards (3 days vs 7 days)? -- Should consumed records be deleted immediately or retained until expiry? -- Should App Clip show a minimal card preview or a full card layout? -- Do we want a “Copy contact info” fallback if Contacts permission is denied? - -## App Clip UI Flow (Screen-by-Screen) -1. **Loading** - - Title: “Loading card…” - - Show spinner and a short message. - - Retry button only if fetch fails. -2. **Card Preview** - - Photo (if available), name, role, company. - - Primary button: “Add to Contacts”. - - Secondary action (optional): “Open Full App”. -3. **Contacts Permission** - - If permission not granted, show prompt messaging. - - Provide “Continue” → triggers permission request. -4. **Saving** - - Progress indicator with short status text. -5. **Success** - - Confirmation text: “Saved to Contacts”. - - Optional button: “Open Full App”. -6. **Failure** - - Short error summary + “Try Again”. - - If expired, show “This card has expired”. - -## App Store Connect Checklist (App Clip) -- Register App Clip in App Store Connect. -- Add App Clip Experience: - - Title, subtitle, and invocation URL. - - Provide header image and action button text. -- Configure Advanced Experiences (optional). -- Associate App Clip to the main app version. -- Verify Associated Domains in the app target. -- Upload build with App Clip and validate with Apple. -- Test invocation on-device with QR and direct URL. - -## Edge Cases -- No photo data → vCard should still import. -- Large photo → ensure CloudKit asset and vCard remain within limits. -- Multiple scans of same QR → record should still fetch until expired. -- Offline scanning → show “Requires internet to load” message. - -## Files To Add (Expected) -- `BusinessCard/Protocols/SharedCardUploading.swift` -- `BusinessCard/Protocols/SharedCardFetching.swift` -- `BusinessCard/Protocols/SharedCardCleaning.swift` -- `BusinessCard/Services/SharedCardCloudKitService.swift` -- `BusinessCard/State/ShareCardState.swift` (or extend existing state file) -- `BusinessCard/Views/ShareCardView.swift` (update only) -- `BusinessCardAppClip/Models/SharedCardSnapshot.swift` -- `BusinessCardAppClip/State/AppClipCardStore.swift` -- `BusinessCardAppClip/Services/AppClipCloudKitService.swift` -- `BusinessCardAppClip/Views/AppClipRootView.swift` - -## Documentation Updates (When Implementation Starts) -- Update `README.md`, `ai_implementation.md`, `Agents.md` together. -- Update `ROADMAP.md` for App Clip progress. diff --git a/README.md b/README.md index 0c41261..571f4e5 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,8 @@ App-specific extensions are in `Design/DesignConstants.swift`. BusinessCard/ ├── Assets.xcassets/ │ └── SocialSymbols/ # Custom brand icons (LinkedIn, X, Instagram, etc.) +├── Configuration/ # App identifiers and configuration +│ └── AppIdentifiers.swift # Centralized company identifiers ├── Design/ # Design constants (extends Bedrock) ├── Localization/ # String helpers ├── Models/ @@ -197,6 +199,16 @@ Without this, the iOS app installs but the watch app does NOT install on the pai `iCloud.com.mbrucedogs.BusinessCard` +### App Identifiers + +All company-specific identifiers are centralized in `Configuration/AppIdentifiers.swift`: + +- Bundle IDs: `AppIdentifiers.bundleIdentifier`, `.watchBundleIdentifier`, `.appClipBundleIdentifier` +- CloudKit: `AppIdentifiers.cloudKitContainerIdentifier` +- App Group: `AppIdentifiers.appGroupIdentifier` + +See `DevAccount-Migration.md` for migration instructions. + ## Notes - Share URLs are sample placeholders diff --git a/XAI-Plan-AppClip.md b/XAI-Plan-AppClip.md deleted file mode 100644 index a557a03..0000000 --- a/XAI-Plan-AppClip.md +++ /dev/null @@ -1,234 +0,0 @@ -# App Clip Implementation Plan for BusinessCard App - -This document outlines the detailed steps to implement the high-priority App Clip feature for instant card sharing, as identified in ROADMAP.md. The goal is to allow recipients to scan a QR code and instantly view/add a full business card (including photo) via an App Clip, without needing to install the full app. This uses CloudKit for temporary storage of a vCard 3.0 payload (including PHOTO field) to address QR code size limitations for large data like images. - -The plan follows the phases from ROADMAP.md, with specific code snippets, file changes, and architecture notes. No actual code changes will be made until this plan is reviewed and approved. All new code will adhere to the guidelines in Agents.md: clean architecture, POP, one type per file, SwiftUI with @Observable, etc. - -## User Flow - -### Sender Flow (Main App) -1. User selects a card and chooses "Share via App Clip". -2. App generates vCard 3.0 payload with PHOTO and uploads to CloudKit. -3. App generates QR code with App Clip URL pointing to the CloudKit record. -4. User shares/shows the QR code. - -### Recipient Flow (App Clip) -1. Recipient scans QR code, launching App Clip. -2. App Clip fetches vCard data from CloudKit using the URL's ID. -3. App Clip parses vCard and displays preview (name, photo, role, company, etc.). -4. Recipient taps "Add to Contacts", parsing vCard to CNContact and saving to contacts app. -5. Optional: Mark record as saved or delete from CloudKit. - -## Sequence Diagram (Text-Based) - -``` -Sender (Main App) CloudKit Recipient (App Clip) Contacts App -|-------------------| |----------------| |---------------------| |------------| - -1. Generate vCard 3.0 w/ PHOTO - | - |---> Upload SharedCard (vCardData) - | - |<--- Record ID - -2. Generate QR with App Clip URL + ID - -[QR Shared/Scanned] - - 3. Launch App Clip w/ URL - | - |---> Fetch SharedCard by ID - | - |<--- vCardData - -4. Parse vCard to display preview - -5. User taps "Add to Contacts" - | - |---> Parse vCard to CNContact - | - |---> Save Contact - | - |<--- Success -``` - -## Phase 1: CloudKit Setup - -### Steps -1. **Enable CloudKit Capability**: - - In Xcode, go to the iOS target Signing & Capabilities tab. - - Add iCloud capability and enable CloudKit. - - Ensure the container is `iCloud.com.mbrucedogs.BusinessCard` (matches existing). - -2. **Create SharedCard Model**: - - Add a new file: `Models/SharedCard.swift`. - - This is a CloudKit-backed model for ephemeral card sharing. - - Properties: full vCard 3.0 data (with PHOTO), expiration date. - - **Code Snippet (SharedCard.swift)**: - ```swift - import CloudKit - import SwiftData - - @Model - final class SharedCard { - @Attribute(.unique) var id: UUID - var vCardData: Data // Full vCard 3.0 payload including PHOTO - var expiresAt: Date - - init(id: UUID = UUID(), vCardData: Data, expiresAt: Date) { - self.id = id - self.vCardData = vCardData - self.expiresAt = expiresAt - } - } - ``` - -3. **Implement Upload Function**: - - Add to `Services/ShareLinkService.swift` (or create a new `CloudKitService.swift` if better separation). - - Function to upload a SharedCard with vCard 3.0 (including PHOTO) and return a share URL. - - **Code Snippet (in ShareLinkService.swift)**: - ```swift - func uploadSharedCard(_ card: BusinessCard) async throws -> URL { - let vCardPayload = try VCardFileService.generateVCardData(for: card, includePhoto: true) // vCard 3.0 with PHOTO field - - let sharedCard = SharedCard( - vCardData: vCardPayload, - expiresAt: Date().addingTimeInterval(60 * 60 * 24) // 24 hours - ) - - // Assuming SwiftData context - modelContext.insert(sharedCard) - try await modelContext.save() // This syncs to CloudKit - - // Generate URL like appclips://yourapp.com/shared/\(sharedCard.id) - return URL(string: "https://yourapp.com/appclip/shared/\(sharedCard.id.uuidString)")! - } - ``` - -4. **Implement Cleanup**: - - In `AppState.swift` or app launch, query and delete expired SharedCards. - - **Code Snippet**: - ```swift - func cleanupExpiredSharedCards() async { - let predicate = #Predicate { $0.expiresAt < Date() } - let descriptor = FetchDescriptor(predicate: predicate) - if let expired = try? modelContext.fetch(descriptor) { - for card in expired { - modelContext.delete(card) - } - try? modelContext.save() - } - } - ``` - -### Documentation Updates -- Update README.md: Add section on App Clip sharing, emphasizing vCard with photo. -- Update ai_implementation.md: Add SharedCard model and CloudKit/vCard notes. -- Update Agents.md: Add guidelines for CloudKit and vCard 3.0 usage. - -## Phase 2: App Clip Target - -### Steps -1. **Create App Clip Target**: - - In Xcode: File > New > Target > App Clip. - - Name: BusinessCardAppClip. - - Bundle ID: com.mbrucedogs.BusinessCard.appclip. - - Keep size under 15MB (minimal UI, no heavy assets). - -2. **Configure Associated Domains**: - - Add `appclips:yourapp.com` to entitlements. - - Set up server-side .well-known/apple-app-site-association file. - -3. **Build Minimal UI**: - - Create `AppClipView.swift`: Display parsed vCard preview with "Add to Contacts" button. - - Use SwiftUI for quick loading. Parse vCard for display. - - **Code Snippet (AppClipView.swift)**: - ```swift - import SwiftUI - import Contacts - - struct AppClipView: View { - let sharedCard: SharedCard // Fetched from CloudKit - @State private var parsedContact: CNContact? // Parsed from vCard - - var body: some View { - VStack { - if let contact = parsedContact { - Text(contact.givenName + " " + contact.familyName).font(.title) - if let photoData = contact.imageData, let image = UIImage(data: photoData) { - Image(uiImage: image).resizable().scaledToFit().frame(width: 100, height: 100) - } - Text(contact.jobTitle).font(.subheadline) - Text(contact.organizationName) - Button("Add to Contacts") { - addToContacts(contact) - } - } else { - Text("Loading...") - } - } - .onAppear { - parsedContact = parseVCard(sharedCard.vCardData) - } - } - - private func addToContacts(_ contact: CNContact) { - let store = CNContactStore() - let saveRequest = CNSaveRequest() - saveRequest.add(contact.mutableCopy() as! CNMutableContact, toContainerWithIdentifier: nil) - try? store.execute(saveRequest) - } - - private func parseVCard(_ data: Data) -> CNContact? { - return try? CNContactVCardSerialization.contacts(with: data).first - } - } - ``` - -4. **Fetch from CloudKit**: - - In App Clip's entry point, parse URL parameter to fetch SharedCard by ID. - -### Phase 3: Main App Integration - -1. **Add Share Option**: - - In `Views/ShareCardView.swift`, add a new button for "Share via App Clip". - - **Code Snippet**: - ```swift - Button("Share via App Clip") { - Task { - let url = try await shareLinkService.uploadSharedCard(currentCard) - // Generate QR with url - qrCodeImage = QRCodeGenerator.generateQRCode(from: url.absoluteString) - } - } - ``` - -2. **Generate QR Code**: - - Use existing QRCodeGenerator from Bedrock. The QR encodes the App Clip URL, which loads the full vCard from CloudKit. - -### Phase 4: Testing and Configuration - -- Test on device with App Clip invocation, verifying photo inclusion in saved contacts. -- Configure in App Store Connect. -- Add unit tests for vCard generation (with PHOTO), upload, fetch, parsing, and cleanup. - -## Additional Considerations -- **Security**: Ensure CloudKit records are public-readable but short-lived. Add authentication if needed for private shares. -- **Error Handling**: Handle cases like expired records, parsing failures, or no internet in App Clip. -- **Performance**: Compress photos before including in vCard PHOTO field. -- **Accessibility**: Ensure App Clip UI supports VoiceOver and Dynamic Type. -- **Localization**: Add strings for App Clip to String Catalogs. - -## Risks and Considerations -- CloudKit quotas and costs for storing vCard data with photos. -- Ensure vCard payloads are optimized/compressed. -- Handle parsing errors for vCard data. -- Update all three docs (Agents.md, README.md, ai_implementation.md) upon completion. -- Use vCard 3.0 format with PHOTO field to include images in saved contacts, addressing QR code size limitations for large data like photos. - -This plan can be referenced as `XAI-Plan-AppClip.md`. If approved, we can proceed to implementation in ACT MODE. diff --git a/ai_implementation.md b/ai_implementation.md index 2578d77..9931f49 100644 --- a/ai_implementation.md +++ b/ai_implementation.md @@ -55,6 +55,15 @@ App-specific extensions are in `Design/DesignConstants.swift`: ## Important Files +### Configuration + +- `Configuration/AppIdentifiers.swift` — Centralized company identifiers: + - `companyIdentifier` — Base identifier (e.g., "com.mbrucedogs") + - Derived: `bundleIdentifier`, `watchBundleIdentifier`, `appClipBundleIdentifier` + - Derived: `appGroupIdentifier`, `cloudKitContainerIdentifier` + - App Clip: `appClipDomain`, `appClipURL(recordName:)` + - **Migration**: Change `companyIdentifier` + update entitlements. See `DevAccount-Migration.md`. + ### Models - `Models/BusinessCard.swift` — SwiftData model with: