craft-native 0.0.144 → 0.0.146

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.
package/dist/cli.js CHANGED
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3303
3303
  `);
3304
3304
  }
3305
3305
  // package.json
3306
- var version = "0.0.144";
3306
+ var version = "0.0.146";
3307
3307
 
3308
3308
  // bin/cli.ts
3309
3309
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -310,7 +310,7 @@ function craftBinaryNotFoundMessage(triedPath) {
310
310
  `);
311
311
  }
312
312
  // package.json
313
- var version = "0.0.144";
313
+ var version = "0.0.146";
314
314
 
315
315
  // src/sdk-version.ts
316
316
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ function craftBinaryNotFoundMessage(triedPath) {
58
58
  `);
59
59
  }
60
60
  // package.json
61
- var version = "0.0.144";
61
+ var version = "0.0.146";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -6694,17 +6694,14 @@ struct CraftWebView: UIViewRepresentable {
6694
6694
 
6695
6695
  // MARK: - Social Auth (Apple Sign In)
6696
6696
  private func signInWithApple(nonce: String?, callbackId: String?) {
6697
+ // The same sheet a native screen asks for (CraftNativeUI.swift).
6697
6698
  DispatchQueue.main.async {
6698
- self.pendingCallbackId = callbackId
6699
- let provider = ASAuthorizationAppleIDProvider()
6700
- let request = provider.createRequest()
6701
- request.requestedScopes = [.fullName, .email]
6702
- if let nonce, !nonce.isEmpty { request.nonce = nonce }
6703
-
6704
- let controller = ASAuthorizationController(authorizationRequests: [request])
6705
- controller.delegate = self
6706
- controller.presentationContextProvider = self
6707
- controller.performRequests()
6699
+ CraftSignInWithApple.shared.signIn(nonce: nonce) { [weak self] result in
6700
+ switch result {
6701
+ case .success(let value): self?.resolveCallback(callbackId, result: value)
6702
+ case .failure(let failure): self?.rejectCallback(callbackId, error: failure.message, code: failure.code)
6703
+ }
6704
+ }
6708
6705
  }
6709
6706
  }
6710
6707
 
@@ -7968,56 +7965,6 @@ extension CraftWebView.Coordinator: UIDocumentPickerDelegate {
7968
7965
  }
7969
7966
  }
7970
7967
 
7971
- // MARK: - Apple Sign In Delegate
7972
- extension CraftWebView.Coordinator: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
7973
- func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
7974
- if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
7975
- let userId = appleIDCredential.user
7976
- let email = appleIDCredential.email
7977
- let fullName = appleIDCredential.fullName
7978
-
7979
- var name = ""
7980
- if let givenName = fullName?.givenName {
7981
- name = givenName
7982
- }
7983
- if let familyName = fullName?.familyName {
7984
- name += (name.isEmpty ? "" : " ") + familyName
7985
- }
7986
-
7987
- var result: [String: Any] = ["userId": userId]
7988
- if let email = email { result["email"] = email }
7989
- if !name.isEmpty { result["name"] = name }
7990
-
7991
- if let identityToken = appleIDCredential.identityToken,
7992
- let tokenString = String(data: identityToken, encoding: .utf8) {
7993
- result["identityToken"] = tokenString
7994
- }
7995
- // One use, five minutes: the server redeems it at Apple's token
7996
- // endpoint and so reads the identity straight from Apple.
7997
- if let authorizationCode = appleIDCredential.authorizationCode,
7998
- let codeString = String(data: authorizationCode, encoding: .utf8) {
7999
- result["authorizationCode"] = codeString
8000
- }
8001
-
8002
- resolveCallback(pendingCallbackId, result: result)
8003
- }
8004
- pendingCallbackId = nil
8005
- }
8006
-
8007
- func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
8008
- rejectCallback(pendingCallbackId, error: error.localizedDescription)
8009
- pendingCallbackId = nil
8010
- }
8011
-
8012
- func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
8013
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
8014
- let window = windowScene.windows.first else {
8015
- return UIWindow()
8016
- }
8017
- return window
8018
- }
8019
- }
8020
-
8021
7968
  // MARK: - Bluetooth Delegate
8022
7969
  extension CraftWebView.Coordinator: CBCentralManagerDelegate {
8023
7970
  func centralManagerDidUpdateState(_ central: CBCentralManager) {
@@ -247,8 +247,33 @@ final class CraftHybrid: NSObject {
247
247
  fileprivate(set) var pageDepth = 0
248
248
  fileprivate(set) var pageReady = false
249
249
 
250
+ /// Shared keys a native screen wrote, by the page's own key (NSNull for a
251
+ /// removal), until the page has them: it reports each back as its own
252
+ /// write, which is when it leaves this list.
253
+ private var pendingShared: [String: Any] = [:]
254
+
250
255
  private override init() {}
251
256
 
257
+ /// A native screen wrote a key the page shares (`shareStorage`, named
258
+ /// here by its native name): both Keychain copies, and the page's own
259
+ /// storage, now if it is loaded or when it next loads. Nothing for a key
260
+ /// the config does not share.
261
+ func nativeWroteShared(nativeKey: String, value: String?) {
262
+ guard let webKey = config.shareStorage?.first(where: { $0.value == nativeKey })?.key else { return }
263
+ CraftHybridKeychain.set(nativeKey, value: value)
264
+ DispatchQueue.main.async {
265
+ self.pendingShared[webKey] = value ?? NSNull()
266
+ self.flushShared()
267
+ }
268
+ }
269
+
270
+ private func flushShared() {
271
+ guard pageReady, let webView, !pendingShared.isEmpty,
272
+ let data = try? JSONSerialization.data(withJSONObject: pendingShared),
273
+ let json = String(data: data, encoding: .utf8) else { return }
274
+ webView.evaluateJavaScript("window.__craftHybrid && window.__craftHybrid.adopt && window.__craftHybrid.adopt(\(json))")
275
+ }
276
+
252
277
  static var bundleURL: URL? {
253
278
  Bundle.main.url(forResource: "native-screen", withExtension: "js", subdirectory: "dist")
254
279
  ?? Bundle.main.url(forResource: "native-screen", withExtension: "js")
@@ -511,8 +536,26 @@ final class CraftHybrid: NSObject {
511
536
  SHARE.forEach(share);
512
537
  }
513
538
 
539
+ // A shared key a native screen wrote (a sign-in there): into this
540
+ // page's storage, which reports it back as the page's own write, and
541
+ // a `craftsharedstorage` event for whoever holds it in memory.
542
+ function adopt(values) {
543
+ Object.keys(values || {}).forEach(function (key) {
544
+ if (SHARE.indexOf(key) < 0) return;
545
+ var value = values[key];
546
+ try {
547
+ window.sessionStorage.removeItem(key);
548
+ if (value === null) window.localStorage.removeItem(key);
549
+ else window.localStorage.setItem(key, String(value));
550
+ } catch (e) {}
551
+ share(key);
552
+ try { window.dispatchEvent(new CustomEvent('craftsharedstorage', { detail: { key: key, value: value } })); } catch (e) {}
553
+ });
554
+ }
555
+
514
556
  window.__craftHybrid = {
515
557
  match: function (url) { var path = local(url); return path === null ? null : match(path); },
558
+ adopt: adopt,
516
559
  // The shell's own navigation: never handed back to it, and without
517
560
  // the page's slide, since the shell animates the push itself.
518
561
  // Answers once the page has drawn `path` (its router settles before
@@ -593,12 +636,15 @@ final class CraftHybrid: NSObject {
593
636
  for name in names { CraftSnapshots.write(name, json: nil) }
594
637
  case .shareStorage(let key, let value):
595
638
  guard let nativeKey = config.shareStorage?[key] else { return }
639
+ // The page's own write, or its echo of one a native screen made.
640
+ pendingShared.removeValue(forKey: key)
596
641
  CraftHybridKeychain.set(nativeKey, value: value)
597
642
  case .navigated(let path, let direction, let depth, let byHost):
598
643
  pagePath = path
599
644
  pageDepth = depth
600
645
  if direction == "load" { pageReady = true }
601
646
  controller?.pageNavigated(path: path, direction: direction, depth: depth, byHost: byHost)
647
+ if direction == "load" { flushShared() }
602
648
  case .navigateNative(let path, let replace):
603
649
  controller?.showNative(path: path, replace: replace)
604
650
  case .open(let path):
@@ -31,6 +31,11 @@ protocol CraftMusicPlayer: AnyObject {
31
31
  var currentPlaybackTime: TimeInterval { get }
32
32
  /// Called on main whenever the song or the playback state changes.
33
33
  var onChange: (() -> Void)? { get set }
34
+ /// Sound this player makes that iOS cannot see as another app's: only
35
+ /// the simulator's stand-in, which plays nothing yet should read like
36
+ /// the Music app playing. Read before access is allowed, so the real
37
+ /// player answers false without touching the Music app.
38
+ var soundsLikeOtherAudio: Bool { get }
34
39
  func play()
35
40
  func pause()
36
41
  func skipToNextItem()
@@ -139,7 +144,7 @@ final class CraftMusic {
139
144
  var result: [String: Any] = [
140
145
  "available": true,
141
146
  "authorization": authorization,
142
- "otherAudioPlaying": AVAudioSession.sharedInstance().isOtherAudioPlaying,
147
+ "otherAudioPlaying": AVAudioSession.sharedInstance().isOtherAudioPlaying || player.soundsLikeOtherAudio,
143
148
  "timestamp": Int(Date().timeIntervalSince1970 * 1000),
144
149
  "source": NSNull(),
145
150
  "playbackState": "unknown",
@@ -277,6 +282,7 @@ final class CraftMusic {
277
282
  /// `MPMusicPlayerController.systemMusicPlayer`: the Music app's queue.
278
283
  final class CraftSystemMusicPlayer: CraftMusicPlayer {
279
284
  let source = "appleMusic"
285
+ let soundsLikeOtherAudio = false
280
286
  var onChange: (() -> Void)?
281
287
  private var observers: [NSObjectProtocol] = []
282
288
  private var player: MPMusicPlayerController { MPMusicPlayerController.systemMusicPlayer }
@@ -330,8 +336,9 @@ final class CraftSystemMusicPlayer: CraftMusicPlayer {
330
336
 
331
337
  // MARK: - The simulator's stand-in
332
338
 
333
- /// Three made-up songs that play, pause and skip like the Music app, so the
334
- /// bridge, the page's bar and its events can be tried in the simulator.
339
+ /// Three made-up songs that play (from launch), pause and skip like the Music
340
+ /// app, and count as other audio while they play, so the
341
+ /// bridge, a page's offer, its bar and its events can be tried in the simulator.
335
342
  /// Never used on a device (see `CraftMusic.makePlayer`).
336
343
  final class CraftFakeMusicPlayer: CraftMusicPlayer {
337
344
  let source = "simulated"
@@ -344,10 +351,14 @@ final class CraftFakeMusicPlayer: CraftMusicPlayer {
344
351
  ("Cooldown", "Recovery Club", "Easy Days", 187, 0.33),
345
352
  ]
346
353
  private var index = 0
347
- private var state: MPMusicPlaybackState = .paused
354
+ /// Playing from the start, as when someone opens the app with music on:
355
+ /// that is what a page offering its controls waits for.
356
+ private var state: MPMusicPlaybackState = .playing
348
357
  /// Where the song was when it last started or stopped, and when that was.
349
358
  private var offset: TimeInterval = 0
350
- private var startedAt: Date?
359
+ private var startedAt: Date? = Date()
360
+
361
+ var soundsLikeOtherAudio: Bool { state == .playing }
351
362
 
352
363
  var item: CraftMusicItem? {
353
364
  let song = songs[index]
@@ -45,6 +45,9 @@ enum CraftNativeActions {
45
45
  if config.enableDeepLinks { values.append("deepLinks") }
46
46
  if config.enableLocalNotifications { values.append("notifications") }
47
47
  if config.enableHealthKit { values.append("health") }
48
+ // The in-app browser and the system's web sign-in sheet (CraftBrowser).
49
+ values.append("browser")
50
+ if config.enableSocialAuth { values.append("appleSignIn") }
48
51
  return values
49
52
  }
50
53
 
@@ -117,6 +120,11 @@ enum CraftNativeActions {
117
120
  return
118
121
  }
119
122
  sync = secureStorage(method: method, args: args)
123
+ // A key the web page shares (`shareStorage`): the page gets it too,
124
+ // so a sign-in on a native screen signs the page in.
125
+ if case .success = sync, ["set", "remove", "delete"].contains(method), let key = args.first as? String {
126
+ CraftHybrid.shared.nativeWroteShared(nativeKey: key, value: method == "set" ? args.count > 1 ? args[1] as? String : nil : nil)
127
+ }
120
128
  case ("Biometrics", "isAvailable"):
121
129
  sync = config.enableBiometric ? .success(biometricContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)) : failure("CAPABILITY_DISABLED", "Biometric authentication is disabled")
122
130
  case ("Biometrics", "getBiometricType"):
@@ -146,6 +154,32 @@ enum CraftNativeActions {
146
154
  }
147
155
  databaseQueue.async { complete(requestToken, database(method: method, args: args), completion) }
148
156
  return
157
+ case ("Browser", "open"):
158
+ // SFSafariViewController, or for `mode: 'auth'` the system's web
159
+ // sign-in sheet, the same as a page's `craft.browser.open`.
160
+ guard let address = args.first as? String, let url = URL(string: address) else {
161
+ complete(requestToken, failure("INVALID_ARGUMENT", "Browser.open needs a URL it can parse"), completion)
162
+ return
163
+ }
164
+ let options = args.count > 1 ? args[1] as? [String: Any] ?? [:] : [:]
165
+ DispatchQueue.main.async {
166
+ CraftBrowser.shared.open(url, mode: options["mode"] as? String ?? "safari", callbackScheme: options["callbackScheme"] as? String, from: nil) { answer in
167
+ complete(requestToken, .success(answer), completion)
168
+ }
169
+ }
170
+ return
171
+ case ("Auth", "signInWithApple"):
172
+ guard config.enableSocialAuth else {
173
+ complete(requestToken, failure("CAPABILITY_DISABLED", "Social sign-in is disabled"), completion)
174
+ return
175
+ }
176
+ let options = args.first as? [String: Any] ?? [:]
177
+ DispatchQueue.main.async {
178
+ CraftSignInWithApple.shared.signIn(nonce: options["nonce"] as? String) { result in
179
+ complete(requestToken, result.mapError { CraftNativeActionError(code: $0.code, message: $0.message) }.map { $0 as Any }, completion)
180
+ }
181
+ }
182
+ return
149
183
  case ("Dialog", _):
150
184
  // The system's own alert, confirmation and action sheet, the same
151
185
  // ones a page gets (CraftNativeUI.swift), over the native screen.
@@ -1,9 +1,11 @@
1
1
  import Foundation
2
2
  import AVFoundation
3
3
  import AVKit
4
+ import CryptoKit
4
5
  import Darwin
5
6
  import JavaScriptCore
6
7
  import OSLog
8
+ import Security
7
9
  import SwiftUI
8
10
  import UIKit
9
11
 
@@ -2461,6 +2463,89 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
2461
2463
  jsContext.setObject(snapshotSet, forKeyedSubscript: "craftNativeSnapshotSet" as NSString)
2462
2464
  jsContext.setObject(secureGet, forKeyedSubscript: "craftNativeSecureGet" as NSString)
2463
2465
  jsContext.setObject(setOptions, forKeyedSubscript: "craftNativeSetOptions" as NSString)
2466
+ // `crypto` for code shared with the page (a sign-in's nonce and its
2467
+ // SHA-256): JavaScriptCore has none. Random bytes from the system's
2468
+ // generator, digests from CryptoKit, both as hex.
2469
+ let randomBytes: @convention(block) (Int) -> String? = { count in
2470
+ guard count >= 0, count <= 65_536 else { return nil }
2471
+ var bytes = [UInt8](repeating: 0, count: count)
2472
+ guard count == 0 || SecRandomCopyBytes(kSecRandomDefault, count, &bytes) == errSecSuccess else { return nil }
2473
+ return bytes.map { String(format: "%02x", $0) }.joined()
2474
+ }
2475
+ let digest: @convention(block) (String, String) -> String? = { algorithm, hex in
2476
+ var data = Data(capacity: hex.count / 2)
2477
+ var index = hex.startIndex
2478
+ while index < hex.endIndex {
2479
+ let next = hex.index(index, offsetBy: 2, limitedBy: hex.endIndex) ?? hex.endIndex
2480
+ guard let byte = UInt8(hex[index..<next], radix: 16) else { return nil }
2481
+ data.append(byte)
2482
+ index = next
2483
+ }
2484
+ let bytes: [UInt8]
2485
+ switch algorithm.uppercased() {
2486
+ case "SHA-1": bytes = Array(Insecure.SHA1.hash(data: data))
2487
+ case "SHA-256": bytes = Array(SHA256.hash(data: data))
2488
+ case "SHA-384": bytes = Array(SHA384.hash(data: data))
2489
+ case "SHA-512": bytes = Array(SHA512.hash(data: data))
2490
+ default: return nil
2491
+ }
2492
+ return bytes.map { String(format: "%02x", $0) }.joined()
2493
+ }
2494
+ jsContext.setObject(randomBytes, forKeyedSubscript: "craftNativeRandomBytes" as NSString)
2495
+ jsContext.setObject(digest, forKeyedSubscript: "craftNativeDigest" as NSString)
2496
+ jsContext.evaluateScript("""
2497
+ (function() {
2498
+ var cryptoObject = globalThis.crypto || {};
2499
+ function hexToBytes(hex) {
2500
+ var bytes = new Uint8Array(hex.length / 2);
2501
+ for (var i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
2502
+ return bytes;
2503
+ }
2504
+ function toHex(bytes) {
2505
+ var out = '';
2506
+ for (var i = 0; i < bytes.length; i++) out += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);
2507
+ return out;
2508
+ }
2509
+ function viewOf(data) {
2510
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
2511
+ if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
2512
+ throw new TypeError('crypto.subtle.digest needs an ArrayBuffer or a typed array');
2513
+ }
2514
+ if (typeof cryptoObject.getRandomValues !== 'function') {
2515
+ cryptoObject.getRandomValues = function(array) {
2516
+ if (!ArrayBuffer.isView(array)) throw new TypeError('crypto.getRandomValues needs a typed array');
2517
+ var hex = craftNativeRandomBytes(array.byteLength);
2518
+ if (hex == null) throw new Error('No random bytes from the system');
2519
+ new Uint8Array(array.buffer, array.byteOffset, array.byteLength).set(hexToBytes(hex));
2520
+ return array;
2521
+ };
2522
+ }
2523
+ if (typeof cryptoObject.randomUUID !== 'function') {
2524
+ cryptoObject.randomUUID = function() {
2525
+ var b = cryptoObject.getRandomValues(new Uint8Array(16));
2526
+ b[6] = (b[6] & 0x0f) | 0x40;
2527
+ b[8] = (b[8] & 0x3f) | 0x80;
2528
+ var h = toHex(b);
2529
+ return h.slice(0, 8) + '-' + h.slice(8, 12) + '-' + h.slice(12, 16) + '-' + h.slice(16, 20) + '-' + h.slice(20);
2530
+ };
2531
+ }
2532
+ if (!cryptoObject.subtle || typeof cryptoObject.subtle.digest !== 'function') {
2533
+ cryptoObject.subtle = Object.assign(cryptoObject.subtle || {}, {
2534
+ digest: function(algorithm, data) {
2535
+ try {
2536
+ var name = typeof algorithm === 'string' ? algorithm : algorithm && algorithm.name;
2537
+ var out = craftNativeDigest(String(name || ''), toHex(viewOf(data)));
2538
+ if (out == null) return Promise.reject(new Error('Unsupported digest: ' + name));
2539
+ return Promise.resolve(hexToBytes(out).buffer);
2540
+ } catch (error) {
2541
+ return Promise.reject(error);
2542
+ }
2543
+ }
2544
+ });
2545
+ }
2546
+ globalThis.crypto = cryptoObject;
2547
+ })();
2548
+ """)
2464
2549
  jsContext.evaluateScript("""
2465
2550
  (function() {
2466
2551
  function text(value) {
@@ -390,6 +390,74 @@ extension CraftBrowser: ASWebAuthenticationPresentationContextProviding {
390
390
  }
391
391
  }
392
392
 
393
+ // MARK: - Sign in with Apple
394
+
395
+ struct CraftSignInWithAppleError: Error {
396
+ let code: String
397
+ let message: String
398
+ }
399
+
400
+ /// `craft.signInWithApple`: the system's Sign in with Apple sheet, for a page
401
+ /// and for a native screen alike. Answers with Apple's user id, the name and
402
+ /// email when Apple shares them (the first time only), the identity token and
403
+ /// the one-use authorization code a server redeems at Apple. `nonce` is what
404
+ /// the caller wants written into the identity token (its SHA-256, by
405
+ /// convention).
406
+ final class CraftSignInWithApple: NSObject {
407
+ static let shared = CraftSignInWithApple()
408
+
409
+ private var completion: ((Result<[String: Any], CraftSignInWithAppleError>) -> Void)?
410
+ private var controller: ASAuthorizationController?
411
+
412
+ func signIn(nonce: String?, completion: @escaping (Result<[String: Any], CraftSignInWithAppleError>) -> Void) {
413
+ // One sheet at a time: a second request answers the first.
414
+ self.completion?(.failure(CraftSignInWithAppleError(code: "CANCELLED", message: "Another sign-in started")))
415
+ self.completion = completion
416
+ let request = ASAuthorizationAppleIDProvider().createRequest()
417
+ request.requestedScopes = [.fullName, .email]
418
+ if let nonce, !nonce.isEmpty { request.nonce = nonce }
419
+ let controller = ASAuthorizationController(authorizationRequests: [request])
420
+ controller.delegate = self
421
+ controller.presentationContextProvider = self
422
+ self.controller = controller
423
+ controller.performRequests()
424
+ }
425
+
426
+ private func finish(_ result: Result<[String: Any], CraftSignInWithAppleError>) {
427
+ let completion = self.completion
428
+ self.completion = nil
429
+ controller = nil
430
+ completion?(result)
431
+ }
432
+ }
433
+
434
+ extension CraftSignInWithApple: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
435
+ func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
436
+ guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {
437
+ finish(.failure(CraftSignInWithAppleError(code: "SIGN_IN_FAILED", message: "Apple answered without an Apple ID")))
438
+ return
439
+ }
440
+ var result: [String: Any] = ["userId": credential.user]
441
+ if let email = credential.email { result["email"] = email }
442
+ let name = [credential.fullName?.givenName, credential.fullName?.familyName].compactMap { $0 }.joined(separator: " ")
443
+ if !name.isEmpty { result["name"] = name }
444
+ if let token = credential.identityToken.flatMap({ String(data: $0, encoding: .utf8) }) { result["identityToken"] = token }
445
+ // One use, five minutes: the server redeems it at Apple's token
446
+ // endpoint and so reads the identity straight from Apple.
447
+ if let code = credential.authorizationCode.flatMap({ String(data: $0, encoding: .utf8) }) { result["authorizationCode"] = code }
448
+ finish(.success(result))
449
+ }
450
+
451
+ func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
452
+ let cancelled = (error as? ASAuthorizationError)?.code == .canceled
453
+ finish(.failure(CraftSignInWithAppleError(code: cancelled ? "CANCELLED" : "SIGN_IN_FAILED", message: error.localizedDescription)))
454
+ }
455
+
456
+ func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
457
+ CraftPresenter.keyWindow() ?? ASPresentationAnchor()
458
+ }
459
+ }
460
+
393
461
  // MARK: - SF Symbols
394
462
 
395
463
  /// `craft.symbols.image`: an SF Symbol drawn to a PNG data URL, so a page can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.144",
3
+ "version": "0.0.146",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",