Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
8596a699c1
commit
3007805011
@ -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 = "<group>";
|
||||
};
|
||||
EA8379252F105F2600077F87 /* BusinessCard */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
@ -68,6 +64,11 @@
|
||||
path = BusinessCardUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EA837F992F11B16400077F87 /* BusinessCardWatch Watch App */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "BusinessCardWatch Watch App";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 = "<group>";
|
||||
@ -121,6 +129,7 @@
|
||||
60186E73BC8040538616865B /* BusinessCardWatch.app */,
|
||||
EA8379302F105F2800077F87 /* BusinessCardTests.xctest */,
|
||||
EA83793A2F105F2800077F87 /* BusinessCardUITests.xctest */,
|
||||
EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@ -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 */
|
||||
|
||||
@ -7,12 +7,17 @@
|
||||
<key>BusinessCard.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>BusinessCardWatch.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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
|
||||
)
|
||||
]
|
||||
}
|
||||
@ -29,5 +29,6 @@
|
||||
"fr-CA" : { "stringUnit" : { "state" : "translated", "value" : "Sélectionnée" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version" : "1.1"
|
||||
}
|
||||
@ -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)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
51
ROADMAP.md
51
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*
|
||||
|
||||
Loading…
Reference in New Issue
Block a user