Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
2b679b0167
commit
fa3cb3e017
11
BusinessCard.code-workspace
Normal file
11
BusinessCard.code-workspace
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../_Packages/Bedrock"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
@ -8,7 +8,9 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
EA69DC822F3C199C00592220 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA69DC812F3C199C00592220 /* Bedrock */; };
|
||||
EA69E9272F3D4B5700592220 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA69E9262F3D4B5700592220 /* Bedrock */; };
|
||||
EA7568D32F4639EE006196BB /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA7568D22F4639EE006196BB /* Bedrock */; };
|
||||
EA7568D52F463A28006196BB /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA7568D42F463A28006196BB /* Bedrock */; };
|
||||
EA7568D72F463A2E006196BB /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA7568D62F463A2E006196BB /* Bedrock */; };
|
||||
EA837E672F107D6800077F87 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA837E662F107D6800077F87 /* Bedrock */; };
|
||||
EAAE892A2F12DE110075BC8A /* BusinessCardWatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
EACLIP0012F200000000001 /* BusinessCardClip.app in Embed App Clips */ = {isa = PBXBuildFile; fileRef = EACLIP0012F200000000002 /* BusinessCardClip.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
@ -150,6 +152,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EA7568D32F4639EE006196BB /* Bedrock in Frameworks */,
|
||||
EA837E672F107D6800077F87 /* Bedrock in Frameworks */,
|
||||
EA69DC822F3C199C00592220 /* Bedrock in Frameworks */,
|
||||
);
|
||||
@ -173,6 +176,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EA7568D72F463A2E006196BB /* Bedrock in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -180,7 +184,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EA69E9272F3D4B5700592220 /* Bedrock in Frameworks */,
|
||||
EA7568D52F463A28006196BB /* Bedrock in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -255,6 +259,7 @@
|
||||
packageProductDependencies = (
|
||||
EA837E662F107D6800077F87 /* Bedrock */,
|
||||
EA69DC812F3C199C00592220 /* Bedrock */,
|
||||
EA7568D22F4639EE006196BB /* Bedrock */,
|
||||
);
|
||||
productName = BusinessCard;
|
||||
productReference = EA8379232F105F2600077F87 /* Business Card.app */;
|
||||
@ -323,6 +328,7 @@
|
||||
);
|
||||
name = "BusinessCardWatch Watch App";
|
||||
packageProductDependencies = (
|
||||
EA7568D62F463A2E006196BB /* Bedrock */,
|
||||
);
|
||||
productName = "BusinessCardWatch Watch App";
|
||||
productReference = EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */;
|
||||
@ -345,7 +351,7 @@
|
||||
);
|
||||
name = BusinessCardClip;
|
||||
packageProductDependencies = (
|
||||
EA69E9262F3D4B5700592220 /* Bedrock */,
|
||||
EA7568D42F463A28006196BB /* Bedrock */,
|
||||
);
|
||||
productName = BusinessCardClip;
|
||||
productReference = EACLIP0012F200000000002 /* BusinessCardClip.app */;
|
||||
@ -392,7 +398,7 @@
|
||||
mainGroup = EA83791A2F105F2600077F87;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
EA69DC802F3C199C00592220 /* XCLocalSwiftPackageReference "../Bedrock" */,
|
||||
EA7568D12F4639EE006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = EA8379242F105F2600077F87 /* Products */;
|
||||
@ -1001,9 +1007,9 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
EA69DC802F3C199C00592220 /* XCLocalSwiftPackageReference "../Bedrock" */ = {
|
||||
EA7568D12F4639EE006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = ../Bedrock;
|
||||
relativePath = ../_Packages/Bedrock;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
@ -1012,9 +1018,18 @@
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Bedrock;
|
||||
};
|
||||
EA69E9262F3D4B5700592220 /* Bedrock */ = {
|
||||
EA7568D22F4639EE006196BB /* Bedrock */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = EA69DC802F3C199C00592220 /* XCLocalSwiftPackageReference "../Bedrock" */;
|
||||
productName = Bedrock;
|
||||
};
|
||||
EA7568D42F463A28006196BB /* Bedrock */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = EA7568D12F4639EE006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */;
|
||||
productName = Bedrock;
|
||||
};
|
||||
EA7568D62F463A2E006196BB /* Bedrock */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = EA7568D12F4639EE006196BB /* XCLocalSwiftPackageReference "../_Packages/Bedrock" */;
|
||||
productName = Bedrock;
|
||||
};
|
||||
EA837E662F107D6800077F87 /* Bedrock */ = {
|
||||
|
||||
@ -53,7 +53,7 @@
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "--clip-debug=preview"
|
||||
isEnabled = "YES">
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--clip-debug=loading"
|
||||
@ -68,6 +68,13 @@
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "_XCAppClipURL"
|
||||
value = "https://topdoglabs.com/bc?id=FE866C3C-EEBC-4A66-B176-11D0F01E28DF"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<key>BusinessCard.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>BusinessCardClip.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
@ -17,7 +17,7 @@
|
||||
<key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
||||
@ -817,6 +817,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Show first-run onboarding again on next app launch" : {
|
||||
"comment" : "A description of the reset onboarding feature.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Social Media" : {
|
||||
|
||||
},
|
||||
|
||||
@ -1,38 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
"colors": [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.860",
|
||||
"green" : "0.470",
|
||||
"red" : "0.220"
|
||||
"color": {
|
||||
"color-space": "srgb",
|
||||
"components": {
|
||||
"alpha": "1.000",
|
||||
"blue": "0.280",
|
||||
"green": "0.330",
|
||||
"red": "0.950"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
"idiom": "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
"appearances": [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.950",
|
||||
"green" : "0.650",
|
||||
"red" : "0.350"
|
||||
"color": {
|
||||
"color-space": "srgb",
|
||||
"components": {
|
||||
"alpha": "1.000",
|
||||
"blue": "0.280",
|
||||
"green": "0.330",
|
||||
"red": "0.950"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
"idiom": "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import SwiftUI
|
||||
import AppIntents
|
||||
import Bedrock
|
||||
|
||||
@main
|
||||
struct BusinessCardClipApp: App {
|
||||
@ -37,30 +38,50 @@ struct BusinessCardClipApp: App {
|
||||
#endif
|
||||
}
|
||||
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
|
||||
Design.debugLog("Clip: onContinueUserActivity fired, webpageURL=\(activity.webpageURL?.absoluteString ?? "nil")")
|
||||
handleUserActivity(activity)
|
||||
}
|
||||
.onOpenURL { url in
|
||||
Design.debugLog("Clip: onOpenURL fired, url=\(url.absoluteString)")
|
||||
handleURL(url)
|
||||
}
|
||||
.task {
|
||||
Design.debugLog("Clip: .task running - checking launch sources")
|
||||
#if DEBUG
|
||||
debugState = parseDebugStateArgument()
|
||||
if let dbg = parseDebugStateArgument() {
|
||||
Design.debugLog("Clip: debug state from args: \(dbg.rawValue)")
|
||||
debugState = dbg
|
||||
return
|
||||
}
|
||||
#endif
|
||||
if let envURL = ProcessInfo.processInfo.environment["_XCAppClipURL"],
|
||||
let url = URL(string: envURL) {
|
||||
Design.debugLog("Clip: _XCAppClipURL from env: \(envURL)")
|
||||
handleURL(url)
|
||||
} else {
|
||||
Design.debugLog("Clip: no _XCAppClipURL in env, no debug args. recordName=\(recordName ?? "nil")")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleUserActivity(_ activity: NSUserActivity) {
|
||||
guard let url = activity.webpageURL else { return }
|
||||
Design.debugLog("Clip: handleUserActivity, webpageURL=\(activity.webpageURL?.absoluteString ?? "nil")")
|
||||
guard let url = activity.webpageURL else {
|
||||
Design.debugLog("Clip: handleUserActivity - no webpageURL, skipping")
|
||||
return
|
||||
}
|
||||
handleURL(url)
|
||||
}
|
||||
|
||||
private func handleURL(_ url: URL) {
|
||||
Design.debugLog("Clip: handleURL \(url.absoluteString)")
|
||||
guard let id = extractRecordName(from: url) else {
|
||||
Design.debugLog("Clip: extractRecordName failed for \(url.absoluteString)")
|
||||
launchErrorMessage = ClipError.invalidRecord.localizedDescription
|
||||
return
|
||||
}
|
||||
|
||||
Design.debugLog("Clip: extracted recordName=\(id), setting recordName")
|
||||
launchErrorMessage = nil
|
||||
recordName = id
|
||||
}
|
||||
|
||||
@ -43,12 +43,12 @@ final class ClipCardStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the currently loaded card to Contacts.
|
||||
/// Saves the currently loaded card to Contacts via CNContactStore.
|
||||
/// Note: App Clips cannot use CNContactStore; use the share sheet flow instead.
|
||||
func saveToContacts() async {
|
||||
guard let snapshot else { return }
|
||||
do {
|
||||
try await contactSave.saveContact(vCardData: snapshot.vCardData)
|
||||
// Best-effort cleanup so saved cards do not linger in public CloudKit.
|
||||
try? await cloudKit.deleteSharedCard(recordName: snapshot.recordName)
|
||||
state = .saved
|
||||
} catch {
|
||||
@ -56,6 +56,13 @@ final class ClipCardStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort cleanup after user shares the vCard (e.g. via share sheet).
|
||||
/// App Clips use the share sheet instead of CNContactStore.
|
||||
func cleanupAfterShare() async {
|
||||
guard let snapshot else { return }
|
||||
try? await cloudKit.deleteSharedCard(recordName: snapshot.recordName)
|
||||
}
|
||||
|
||||
private func userMessage(for error: Error, fallback: ClipError) -> String {
|
||||
if let clipError = error as? ClipError {
|
||||
return clipError.localizedDescription
|
||||
|
||||
@ -16,7 +16,7 @@ struct ClipRootView: View {
|
||||
ClipLoadingView()
|
||||
case .loaded(let snapshot):
|
||||
ClipCardPreview(snapshot: snapshot) {
|
||||
Task { await store.saveToContacts() }
|
||||
Task { await store.cleanupAfterShare() }
|
||||
}
|
||||
case .saved:
|
||||
ClipSuccessView()
|
||||
|
||||
@ -9,24 +9,25 @@ struct ClipCardPreview: View {
|
||||
}
|
||||
|
||||
let snapshot: SharedCardSnapshot
|
||||
/// Called when share sheet is dismissed; use for CloudKit cleanup.
|
||||
let onSave: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: ClipDesign.Spacing.xLarge) {
|
||||
Spacer(minLength: ClipDesign.Spacing.medium)
|
||||
@State private var showShareSheet = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: ClipDesign.Spacing.xLarge) {
|
||||
previewCard
|
||||
.frame(maxWidth: ClipDesign.Size.previewMaxWidth)
|
||||
.padding(.horizontal, ClipDesign.Spacing.large)
|
||||
|
||||
VStack(spacing: ClipDesign.Spacing.medium) {
|
||||
// Save button
|
||||
// Add to Contacts - uses share sheet because App Clips cannot use CNContactStore
|
||||
ClipPrimaryButton(
|
||||
title: String(localized: "Save to Contacts"),
|
||||
title: String(localized: "Add to Contacts"),
|
||||
systemImage: "person.crop.circle.badge.plus",
|
||||
action: onSave
|
||||
action: { showShareSheet = true }
|
||||
)
|
||||
.accessibilityLabel(Text("Save \(snapshot.displayName) to contacts"))
|
||||
.accessibilityLabel(Text("Add \(snapshot.displayName) to contacts"))
|
||||
|
||||
// Get full app prompt
|
||||
Button {
|
||||
@ -38,8 +39,14 @@ struct ClipCardPreview: View {
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, ClipDesign.Spacing.xLarge)
|
||||
|
||||
Spacer(minLength: ClipDesign.Spacing.large)
|
||||
}
|
||||
.padding(.horizontal, ClipDesign.Spacing.large)
|
||||
}
|
||||
.sheet(isPresented: $showShareSheet) {
|
||||
ClipShareSheet(vCardData: snapshot.vCardData) {
|
||||
showShareSheet = false
|
||||
onSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ struct ClipPrimaryButton: View {
|
||||
HStack(spacing: ClipDesign.Spacing.small) {
|
||||
if let systemImage {
|
||||
Image(systemName: systemImage)
|
||||
.foregroundStyle(Color.Clip.background)
|
||||
}
|
||||
Text(title)
|
||||
.styled(.headingEmphasis, emphasis: .custom(Color.Clip.background))
|
||||
|
||||
33
BusinessCardClip/Views/Components/ClipShareSheet.swift
Normal file
33
BusinessCardClip/Views/Components/ClipShareSheet.swift
Normal file
@ -0,0 +1,33 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
/// Presents a share sheet with a vCard file so the user can add to Contacts.
|
||||
/// App Clips cannot use CNContactStore; the share sheet's "Add to Contacts" option works instead.
|
||||
struct ClipShareSheet: UIViewControllerRepresentable {
|
||||
let vCardData: String
|
||||
let onDismiss: (() -> Void)?
|
||||
|
||||
init(vCardData: String, onDismiss: (() -> Void)? = nil) {
|
||||
self.vCardData = vCardData
|
||||
self.onDismiss = onDismiss
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> UIActivityViewController {
|
||||
let tempURL = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("contact-\(UUID().uuidString).vcf")
|
||||
let data = Data(vCardData.utf8)
|
||||
try? data.write(to: tempURL)
|
||||
|
||||
let controller = UIActivityViewController(
|
||||
activityItems: [tempURL],
|
||||
applicationActivities: nil
|
||||
)
|
||||
controller.completionWithItemsHandler = { _, _, _, _ in
|
||||
try? FileManager.default.removeItem(at: tempURL)
|
||||
onDismiss?()
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user