@craft-native/ios 0.0.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,361 @@
1
+ import SwiftUI
2
+ import ActivityKit
3
+ import AppIntents
4
+ import TipKit
5
+
6
+ // MARK: - iOS 16+ Advanced Features
7
+
8
+ /// Live Activities (Dynamic Island) Support
9
+ @available(iOS 16.1, *)
10
+ struct CraftLiveActivity: ActivityAttributes {
11
+ public struct ContentState: Codable, Hashable {
12
+ var status: String
13
+ var progress: Double
14
+ }
15
+
16
+ var name: String
17
+ }
18
+
19
+ @available(iOS 16.1, *)
20
+ class LiveActivityManager {
21
+ static let shared = LiveActivityManager()
22
+
23
+ private var currentActivity: Activity<CraftLiveActivity>?
24
+
25
+ func startActivity(name: String, status: String, progress: Double) async throws {
26
+ let attributes = CraftLiveActivity(name: name)
27
+ let contentState = CraftLiveActivity.ContentState(status: status, progress: progress)
28
+
29
+ currentActivity = try Activity.request(
30
+ attributes: attributes,
31
+ content: .init(state: contentState, staleDate: nil)
32
+ )
33
+ }
34
+
35
+ func updateActivity(status: String, progress: Double) async {
36
+ guard let activity = currentActivity else { return }
37
+
38
+ let contentState = CraftLiveActivity.ContentState(status: status, progress: progress)
39
+ await activity.update(.init(state: contentState, staleDate: nil))
40
+ }
41
+
42
+ func endActivity() async {
43
+ guard let activity = currentActivity else { return }
44
+ await activity.end(nil, dismissalPolicy: .immediate)
45
+ currentActivity = nil
46
+ }
47
+ }
48
+
49
+ // MARK: - App Intents (iOS 16+)
50
+
51
+ @available(iOS 16.0, *)
52
+ struct OpenCraftIntent: AppIntent {
53
+ static var title: LocalizedStringResource = "Open Craft App"
54
+ static var description = IntentDescription("Opens the Craft application")
55
+
56
+ func perform() async throws -> some IntentResult {
57
+ // Open app logic
58
+ return .result()
59
+ }
60
+ }
61
+
62
+ @available(iOS 16.0, *)
63
+ struct CraftShortcuts: AppShortcutsProvider {
64
+ static var appShortcuts: [AppShortcut] {
65
+ AppShortcut(
66
+ intent: OpenCraftIntent(),
67
+ phrases: [
68
+ "Open \(.applicationName)",
69
+ "Launch \(.applicationName)"
70
+ ],
71
+ shortTitle: "Open App",
72
+ systemImageName: "app.fill"
73
+ )
74
+ }
75
+ }
76
+
77
+ // MARK: - TipKit Integration (iOS 17+)
78
+
79
+ @available(iOS 17.0, *)
80
+ struct CraftWelcomeTip: Tip {
81
+ var title: Text {
82
+ Text("Welcome to Craft")
83
+ }
84
+
85
+ var message: Text? {
86
+ Text("Get started by exploring the features")
87
+ }
88
+
89
+ var image: Image? {
90
+ Image(systemName: "star.fill")
91
+ }
92
+ }
93
+
94
+ @available(iOS 17.0, *)
95
+ class TipKitManager {
96
+ static let shared = TipKitManager()
97
+
98
+ func configure() {
99
+ try? Tips.configure([
100
+ .displayFrequency(.immediate),
101
+ .datastoreLocation(.applicationDefault)
102
+ ])
103
+ }
104
+ }
105
+
106
+ // MARK: - SharePlay Integration
107
+
108
+ @available(iOS 15.0, *)
109
+ class SharePlayManager: ObservableObject {
110
+ static let shared = SharePlayManager()
111
+
112
+ @Published var isActive = false
113
+
114
+ func startSharePlay() async throws {
115
+ // SharePlay session logic
116
+ isActive = true
117
+ }
118
+
119
+ func endSharePlay() {
120
+ isActive = false
121
+ }
122
+ }
123
+
124
+ // MARK: - App Clips Support
125
+
126
+ class AppClipManager {
127
+ static let shared = AppClipManager()
128
+
129
+ var isRunningInAppClip: Bool {
130
+ #if APPCLIP
131
+ return true
132
+ #else
133
+ return false
134
+ #endif
135
+ }
136
+
137
+ func configureAppClip() {
138
+ // App Clip specific configuration
139
+ }
140
+ }
141
+
142
+ // MARK: - Focus Filters (iOS 16+)
143
+
144
+ @available(iOS 16.0, *)
145
+ class FocusFilterManager {
146
+ static let shared = FocusFilterManager()
147
+
148
+ func getCurrentFocus() -> String? {
149
+ // Return current Focus mode if available
150
+ return nil
151
+ }
152
+
153
+ func registerFocusFilter() {
154
+ // Register app-specific Focus filter
155
+ }
156
+ }
157
+
158
+ // MARK: - StoreKit 2 Full Implementation
159
+
160
+ @available(iOS 15.0, *)
161
+ class StoreKitManager: ObservableObject {
162
+ static let shared = StoreKitManager()
163
+
164
+ @Published var products: [Product] = []
165
+ @Published var purchasedProductIDs: Set<String> = []
166
+
167
+ private var updateListenerTask: Task<Void, Error>?
168
+
169
+ func loadProducts(productIDs: [String]) async throws {
170
+ products = try await Product.products(for: productIDs)
171
+ }
172
+
173
+ func purchase(_ product: Product) async throws -> Transaction? {
174
+ let result = try await product.purchase()
175
+
176
+ switch result {
177
+ case .success(let verification):
178
+ let transaction = try checkVerified(verification)
179
+ await transaction.finish()
180
+ await updatePurchasedProducts()
181
+ return transaction
182
+
183
+ case .userCancelled, .pending:
184
+ return nil
185
+
186
+ @unknown default:
187
+ return nil
188
+ }
189
+ }
190
+
191
+ func restorePurchases() async throws {
192
+ for await result in Transaction.currentEntitlements {
193
+ let transaction = try checkVerified(result)
194
+ await updatePurchasedProducts()
195
+ }
196
+ }
197
+
198
+ func startObservingTransactions() {
199
+ updateListenerTask = Task.detached {
200
+ for await result in Transaction.updates {
201
+ do {
202
+ let transaction = try self.checkVerified(result)
203
+ await transaction.finish()
204
+ await self.updatePurchasedProducts()
205
+ } catch {
206
+ print("Transaction verification failed: \(error)")
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ func stopObservingTransactions() {
213
+ updateListenerTask?.cancel()
214
+ }
215
+
216
+ private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
217
+ switch result {
218
+ case .unverified:
219
+ throw StoreError.failedVerification
220
+ case .verified(let safe):
221
+ return safe
222
+ }
223
+ }
224
+
225
+ @MainActor
226
+ private func updatePurchasedProducts() async {
227
+ for await result in Transaction.currentEntitlements {
228
+ guard case .verified(let transaction) = result else {
229
+ continue
230
+ }
231
+
232
+ if transaction.revocationDate == nil {
233
+ purchasedProductIDs.insert(transaction.productID)
234
+ } else {
235
+ purchasedProductIDs.remove(transaction.productID)
236
+ }
237
+ }
238
+ }
239
+
240
+ enum StoreError: Error {
241
+ case failedVerification
242
+ }
243
+ }
244
+
245
+ // MARK: - CarPlay Support
246
+
247
+ #if canImport(CarPlay)
248
+ import CarPlay
249
+
250
+ @available(iOS 12.0, *)
251
+ class CarPlayManager: NSObject, CPApplicationDelegate {
252
+ static let shared = CarPlayManager()
253
+
254
+ private var interfaceController: CPInterfaceController?
255
+
256
+ func application(_ application: UIApplication, didConnectCarInterfaceController interfaceController: CPInterfaceController, to window: CPWindow) {
257
+ self.interfaceController = interfaceController
258
+
259
+ let template = CPListTemplate(title: "Craft", sections: [
260
+ CPListSection(items: [
261
+ CPListItem(text: "Home", detailText: "Go to home screen")
262
+ ])
263
+ ])
264
+
265
+ interfaceController.setRootTemplate(template, animated: true)
266
+ }
267
+
268
+ func application(_ application: UIApplication, didDisconnectCarInterfaceController interfaceController: CPInterfaceController, from window: CPWindow) {
269
+ self.interfaceController = nil
270
+ }
271
+ }
272
+ #endif
273
+
274
+ // MARK: - Bridge Integration Extensions
275
+
276
+ extension CraftWebView.Coordinator {
277
+
278
+ @available(iOS 16.1, *)
279
+ func startLiveActivity(name: String, status: String, progress: Double, callbackId: String?) {
280
+ Task {
281
+ do {
282
+ try await LiveActivityManager.shared.startActivity(name: name, status: status, progress: progress)
283
+ resolveCallback(callbackId, result: ["started": true])
284
+ } catch {
285
+ rejectCallback(callbackId, error: error.localizedDescription)
286
+ }
287
+ }
288
+ }
289
+
290
+ @available(iOS 16.1, *)
291
+ func updateLiveActivity(status: String, progress: Double, callbackId: String?) {
292
+ Task {
293
+ await LiveActivityManager.shared.updateActivity(status: status, progress: progress)
294
+ resolveCallback(callbackId, result: ["updated": true])
295
+ }
296
+ }
297
+
298
+ @available(iOS 16.1, *)
299
+ func endLiveActivity(callbackId: String?) {
300
+ Task {
301
+ await LiveActivityManager.shared.endActivity()
302
+ resolveCallback(callbackId, result: ["ended": true])
303
+ }
304
+ }
305
+
306
+ @available(iOS 15.0, *)
307
+ func loadStoreProducts(productIDs: [String], callbackId: String?) {
308
+ Task {
309
+ do {
310
+ try await StoreKitManager.shared.loadProducts(productIDs: productIDs)
311
+ let products = StoreKitManager.shared.products.map { product in
312
+ [
313
+ "id": product.id,
314
+ "displayName": product.displayName,
315
+ "description": product.description,
316
+ "price": product.displayPrice
317
+ ]
318
+ }
319
+ resolveCallback(callbackId, result: products)
320
+ } catch {
321
+ rejectCallback(callbackId, error: error.localizedDescription)
322
+ }
323
+ }
324
+ }
325
+
326
+ @available(iOS 15.0, *)
327
+ func purchaseProduct(productId: String, callbackId: String?) {
328
+ Task {
329
+ do {
330
+ guard let product = StoreKitManager.shared.products.first(where: { $0.id == productId }) else {
331
+ rejectCallback(callbackId, error: "Product not found")
332
+ return
333
+ }
334
+
335
+ if let transaction = try await StoreKitManager.shared.purchase(product) {
336
+ resolveCallback(callbackId, result: [
337
+ "transactionId": transaction.id,
338
+ "productId": transaction.productID,
339
+ "purchased": true
340
+ ])
341
+ } else {
342
+ resolveCallback(callbackId, result: ["purchased": false])
343
+ }
344
+ } catch {
345
+ rejectCallback(callbackId, error: error.localizedDescription)
346
+ }
347
+ }
348
+ }
349
+
350
+ @available(iOS 15.0, *)
351
+ func restorePurchases(callbackId: String?) {
352
+ Task {
353
+ do {
354
+ try await StoreKitManager.shared.restorePurchases()
355
+ resolveCallback(callbackId, result: ["restored": true])
356
+ } catch {
357
+ rejectCallback(callbackId, error: error.localizedDescription)
358
+ }
359
+ }
360
+ }
361
+ }
@@ -0,0 +1,62 @@
1
+ import ActivityKit
2
+ import SwiftUI
3
+ import WidgetKit
4
+
5
+ @main
6
+ struct CraftLiveActivityBundle: WidgetBundle {
7
+ var body: some Widget {
8
+ CraftLiveActivityWidget()
9
+ }
10
+ }
11
+
12
+ struct CraftLiveActivityWidget: Widget {
13
+ var body: some WidgetConfiguration {
14
+ ActivityConfiguration(for: CraftActivityAttributes.self) { context in
15
+ HStack(spacing: 12) {
16
+ Image(systemName: "figure.run")
17
+ .foregroundStyle(.green)
18
+ VStack(alignment: .leading, spacing: 3) {
19
+ Text(context.attributes.title).font(.headline)
20
+ Text(context.state.status).font(.caption).foregroundStyle(.secondary)
21
+ }
22
+ Spacer()
23
+ VStack(alignment: .trailing, spacing: 3) {
24
+ Text(distance(context.state.distanceMeters)).font(.headline.monospacedDigit())
25
+ Text(duration(context.state.durationSeconds)).font(.caption.monospacedDigit())
26
+ }
27
+ }
28
+ .padding()
29
+ .activityBackgroundTint(Color(red: 0, green: 0.24, blue: 0.18))
30
+ .activitySystemActionForegroundColor(.white)
31
+ } dynamicIsland: { context in
32
+ DynamicIsland {
33
+ DynamicIslandExpandedRegion(.leading) {
34
+ Label(context.state.status, systemImage: "figure.run")
35
+ }
36
+ DynamicIslandExpandedRegion(.trailing) {
37
+ Text(distance(context.state.distanceMeters)).monospacedDigit()
38
+ }
39
+ DynamicIslandExpandedRegion(.bottom) {
40
+ ProgressView(value: context.state.progress)
41
+ .tint(.green)
42
+ }
43
+ } compactLeading: {
44
+ Image(systemName: "figure.run")
45
+ } compactTrailing: {
46
+ Text(distance(context.state.distanceMeters)).monospacedDigit()
47
+ } minimal: {
48
+ Image(systemName: "figure.run")
49
+ }
50
+ .keylineTint(.green)
51
+ }
52
+ }
53
+
54
+ private func distance(_ meters: Double) -> String {
55
+ String(format: "%.2f km", meters / 1000)
56
+ }
57
+
58
+ private func duration(_ seconds: Double) -> String {
59
+ let value = max(Int(seconds), 0)
60
+ return String(format: "%02d:%02d", value / 60, value % 60)
61
+ }
62
+ }
@@ -0,0 +1,155 @@
1
+ import SwiftUI
2
+ import WatchConnectivity
3
+
4
+ @main
5
+ struct {{APP_NAME}}WatchApp: App {
6
+ @StateObject private var session = CraftWatchSession()
7
+
8
+ var body: some Scene {
9
+ WindowGroup {
10
+ CraftWatchRecordingView()
11
+ .environmentObject(session)
12
+ }
13
+ }
14
+ }
15
+
16
+ @MainActor
17
+ final class CraftWatchSession: NSObject, ObservableObject {
18
+ @Published private(set) var isPhoneReachable = false
19
+ @Published private(set) var isRecording = false
20
+ @Published private(set) var isPaused = false
21
+ @Published private(set) var distanceMeters = 0.0
22
+ @Published private(set) var durationSeconds = 0.0
23
+ @Published private(set) var status = "Ready"
24
+
25
+ private let session: WCSession?
26
+
27
+ override init() {
28
+ if WCSession.isSupported() {
29
+ session = .default
30
+ } else {
31
+ session = nil
32
+ }
33
+ super.init()
34
+ session?.delegate = self
35
+ session?.activate()
36
+ }
37
+
38
+ func toggleRecording() {
39
+ let action: String
40
+ if !isRecording {
41
+ action = "start"
42
+ } else if isPaused {
43
+ action = "resume"
44
+ } else {
45
+ action = "pause"
46
+ }
47
+ send(action: action)
48
+ }
49
+
50
+ func finishRecording() {
51
+ send(action: "finish")
52
+ }
53
+
54
+ private func send(action: String) {
55
+ guard let session, session.activationState == .activated else {
56
+ status = "Open WildLoop on iPhone"
57
+ return
58
+ }
59
+ let message: [String: Any] = ["type": "recording-control", "action": action]
60
+ if session.isReachable {
61
+ session.sendMessage(message, replyHandler: { [weak self] reply in
62
+ Task { @MainActor in self?.apply(reply) }
63
+ }, errorHandler: { [weak self] _ in
64
+ Task { @MainActor in self?.status = "Could not reach iPhone" }
65
+ })
66
+ } else {
67
+ session.transferUserInfo(message)
68
+ status = "Command queued"
69
+ }
70
+ }
71
+
72
+ private func apply(_ payload: [String: Any]) {
73
+ if let recording = payload["recording"] as? Bool { isRecording = recording }
74
+ if let paused = payload["paused"] as? Bool { isPaused = paused }
75
+ if let distance = payload["distanceMeters"] as? Double { distanceMeters = max(distance, 0) }
76
+ if let duration = payload["durationSeconds"] as? Double { durationSeconds = max(duration, 0) }
77
+ if let value = payload["status"] as? String { status = value }
78
+ }
79
+ }
80
+
81
+ extension CraftWatchSession: WCSessionDelegate {
82
+ nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
83
+ Task { @MainActor in
84
+ isPhoneReachable = session.isReachable
85
+ status = error == nil ? "Ready" : "Connection unavailable"
86
+ apply(session.receivedApplicationContext)
87
+ }
88
+ }
89
+
90
+ nonisolated func sessionReachabilityDidChange(_ session: WCSession) {
91
+ Task { @MainActor in isPhoneReachable = session.isReachable }
92
+ }
93
+
94
+ nonisolated func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
95
+ Task { @MainActor in apply(applicationContext) }
96
+ }
97
+
98
+ nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
99
+ Task { @MainActor in apply(message) }
100
+ }
101
+
102
+ nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
103
+ Task { @MainActor in apply(userInfo) }
104
+ }
105
+ }
106
+
107
+ struct CraftWatchRecordingView: View {
108
+ @EnvironmentObject private var session: CraftWatchSession
109
+
110
+ var body: some View {
111
+ ScrollView {
112
+ VStack(spacing: 12) {
113
+ Image(systemName: "figure.run.circle.fill")
114
+ .font(.system(size: 34))
115
+ .foregroundStyle(.green)
116
+ .accessibilityHidden(true)
117
+ Text(session.status)
118
+ .font(.headline)
119
+ .multilineTextAlignment(.center)
120
+ HStack {
121
+ metric(distance, label: "Distance")
122
+ metric(duration, label: "Time")
123
+ }
124
+ Button(session.isRecording && !session.isPaused ? "Pause" : session.isPaused ? "Resume" : "Start") {
125
+ session.toggleRecording()
126
+ }
127
+ .buttonStyle(.borderedProminent)
128
+ .tint(.green)
129
+ if session.isRecording {
130
+ Button("Finish", role: .destructive) { session.finishRecording() }
131
+ }
132
+ Text(session.isPhoneReachable ? "iPhone connected" : "Commands sync when iPhone reconnects")
133
+ .font(.caption2)
134
+ .foregroundStyle(.secondary)
135
+ .multilineTextAlignment(.center)
136
+ }
137
+ .padding(.horizontal, 8)
138
+ }
139
+ }
140
+
141
+ private var distance: String { String(format: "%.2f km", session.distanceMeters / 1000) }
142
+ private var duration: String {
143
+ let seconds = max(Int(session.durationSeconds), 0)
144
+ return String(format: "%02d:%02d", seconds / 60, seconds % 60)
145
+ }
146
+
147
+ private func metric(_ value: String, label: String) -> some View {
148
+ VStack(spacing: 2) {
149
+ Text(value).font(.headline.monospacedDigit())
150
+ Text(label).font(.caption2).foregroundStyle(.secondary)
151
+ }
152
+ .frame(maxWidth: .infinity)
153
+ .accessibilityElement(children: .combine)
154
+ }
155
+ }