craft-native 0.0.144 → 0.0.145
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 +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +7 -60
- package/dist/ios/templates/CraftHybrid.swift +46 -0
- package/dist/ios/templates/CraftNativeActions.swift +34 -0
- package/dist/ios/templates/CraftNativeScreen.swift +85 -0
- package/dist/ios/templates/CraftNativeUI.swift +68 -0
- package/package.json +1 -1
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.
|
|
3306
|
+
var version = "0.0.145";
|
|
3307
3307
|
|
|
3308
3308
|
// bin/cli.ts
|
|
3309
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -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
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
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):
|
|
@@ -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
|