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

This commit is contained in:
Matt Bruce 2026-01-10 15:03:33 -06:00
parent 4445b59832
commit c449af3806
7 changed files with 90 additions and 629 deletions

View File

@ -419,6 +419,42 @@ If you need different formats for different purposes:
- Name constants semantically: `accent` not `pointSix`, `large` not `sixteen`. - 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 ## Dynamic Type Instructions
- Always support Dynamic Type for accessibility. - Always support Dynamic Type for accessibility.

View File

@ -15,7 +15,7 @@ struct BusinessCardApp: App {
var container: ModelContainer? var container: ModelContainer?
if let appGroupURL = FileManager.default.containerURL( if let appGroupURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.mbrucedogs.BusinessCard" forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier
) { ) {
let storeURL = appGroupURL.appending(path: "BusinessCard.store") let storeURL = appGroupURL.appending(path: "BusinessCard.store")
let config = ModelConfiguration( let config = ModelConfiguration(

View File

@ -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)")
}
}

View File

@ -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:<base64>`.
- 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=<recordID>`
- 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=<recordID>`.
- 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.

View File

@ -147,6 +147,8 @@ App-specific extensions are in `Design/DesignConstants.swift`.
BusinessCard/ BusinessCard/
├── Assets.xcassets/ ├── Assets.xcassets/
│ └── SocialSymbols/ # Custom brand icons (LinkedIn, X, Instagram, etc.) │ └── SocialSymbols/ # Custom brand icons (LinkedIn, X, Instagram, etc.)
├── Configuration/ # App identifiers and configuration
│ └── AppIdentifiers.swift # Centralized company identifiers
├── Design/ # Design constants (extends Bedrock) ├── Design/ # Design constants (extends Bedrock)
├── Localization/ # String helpers ├── Localization/ # String helpers
├── Models/ ├── 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` `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 ## Notes
- Share URLs are sample placeholders - Share URLs are sample placeholders

View File

@ -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<SharedCard> { $0.expiresAt < Date() }
let descriptor = FetchDescriptor<SharedCard>(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.

View File

@ -55,6 +55,15 @@ App-specific extensions are in `Design/DesignConstants.swift`:
## Important Files ## 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
- `Models/BusinessCard.swift` — SwiftData model with: - `Models/BusinessCard.swift` — SwiftData model with: