@craft-native/ios 0.0.244 → 0.0.245
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/index.d.ts
CHANGED
|
@@ -253,6 +253,14 @@ export interface CraftConfig {
|
|
|
253
253
|
* `craft.secureStorage.getSync('auth.token')`. Turns on secure storage.
|
|
254
254
|
*/
|
|
255
255
|
shareStorage?: Record<string, string>;
|
|
256
|
+
/**
|
|
257
|
+
* The secure-storage key the signed-in account's token lives under
|
|
258
|
+
* (default `auth.token`). `craft.session` follows it: a change of it, made
|
|
259
|
+
* by any native screen or by the page through a `shareStorage` key, is
|
|
260
|
+
* `SESSION_CHANGED { key, token, previous }` on every native screen and a
|
|
261
|
+
* `craftsession` event on the page.
|
|
262
|
+
*/
|
|
263
|
+
sessionTokenKey?: string;
|
|
256
264
|
/**
|
|
257
265
|
* Crash and error reporting to bughq (CraftCrashReporter.swift): crashes
|
|
258
266
|
* and hangs (signal handlers, NSException, MetricKit), uncaught JavaScript
|
package/package.json
CHANGED
package/templates/CraftApp.swift
CHANGED
|
@@ -1619,6 +1619,10 @@ struct CraftConfig: Codable {
|
|
|
1619
1619
|
/// Hybrid: web-storage keys the page's own code writes, mirrored into the
|
|
1620
1620
|
/// Keychain under another name for native screens (`{"auth_token": "auth.token"}`).
|
|
1621
1621
|
var shareStorage: [String: String]? = nil
|
|
1622
|
+
/// The secure-storage key the signed-in account's token lives under
|
|
1623
|
+
/// (`auth.token` unless set): `craft.session` follows it, and a change of
|
|
1624
|
+
/// it is `SESSION_CHANGED` on every screen (CraftSession).
|
|
1625
|
+
var sessionTokenKey: String? = nil
|
|
1622
1626
|
/// The bundle route of the app script: a screen-less script that runs for
|
|
1623
1627
|
/// the app's whole life and hears the app's events (CraftHybrid.swift).
|
|
1624
1628
|
var nativeApp: String? = nil
|
|
@@ -488,12 +488,16 @@ final class CraftHybrid: NSObject {
|
|
|
488
488
|
func pageScript() -> String {
|
|
489
489
|
let routeList = (config.nativeScreens ?? [:]).map { ["pattern": $0.key, "screen": $0.value] }
|
|
490
490
|
let share = Array((config.shareStorage ?? [:]).keys).sorted()
|
|
491
|
+
// The page's key for the sign-in token, when it shares one.
|
|
492
|
+
let tokenKey = CraftSession.tokenKey(config)
|
|
493
|
+
let session = (config.shareStorage ?? [:]).filter { $0.value == tokenKey }.map(\.key).sorted().first
|
|
491
494
|
func json(_ value: Any) -> String {
|
|
492
495
|
(try? JSONSerialization.data(withJSONObject: value)).flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
|
|
493
496
|
}
|
|
494
497
|
return Self.pageScriptTemplate
|
|
495
498
|
.replacingOccurrences(of: "__CRAFT_HYBRID_ROUTES__", with: json(routeList))
|
|
496
499
|
.replacingOccurrences(of: "__CRAFT_HYBRID_SHARE__", with: json(share))
|
|
500
|
+
.replacingOccurrences(of: "__CRAFT_HYBRID_SESSION__", with: session.map { json([$0]) } ?? "[]")
|
|
497
501
|
}
|
|
498
502
|
|
|
499
503
|
// CRAFT_HYBRID_PAGE_SCRIPT_START
|
|
@@ -691,13 +695,46 @@ final class CraftHybrid: NSObject {
|
|
|
691
695
|
|
|
692
696
|
// Web storage the config shares with native screens (`shareStorage`).
|
|
693
697
|
var shared = {};
|
|
694
|
-
|
|
698
|
+
// The page's key for the sign-in token (`sessionTokenKey`), if shared.
|
|
699
|
+
var SESSION = (__CRAFT_HYBRID_SESSION__)[0] || null;
|
|
700
|
+
var sessionListeners = [];
|
|
701
|
+
function stored(key) {
|
|
695
702
|
var value = null;
|
|
696
703
|
try { value = window.sessionStorage.getItem(key); } catch (e) {}
|
|
697
704
|
try { if (value === null) value = window.localStorage.getItem(key); } catch (e) {}
|
|
705
|
+
return value;
|
|
706
|
+
}
|
|
707
|
+
function sessionToken(value) { return typeof value === 'string' && value !== '' ? value : null; }
|
|
708
|
+
function share(key) {
|
|
709
|
+
var value = stored(key);
|
|
698
710
|
if (shared[key] === value) return;
|
|
711
|
+
var known = Object.prototype.hasOwnProperty.call(shared, key);
|
|
712
|
+
var before = shared[key];
|
|
699
713
|
shared[key] = value;
|
|
700
714
|
post({ type: 'shareStorage', key: key, value: value });
|
|
715
|
+
// A sign-in, sign-out or another account, made here or on a native
|
|
716
|
+
// screen: `craftsession { key, token, previous }` and craft.session.
|
|
717
|
+
if (key === SESSION && known && sessionToken(before) !== sessionToken(value)) {
|
|
718
|
+
var change = { key: key, token: sessionToken(value), previous: sessionToken(before) };
|
|
719
|
+
sessionListeners.slice().forEach(function (callback) {
|
|
720
|
+
try { callback(change.token, change.previous); } catch (e) { console.error(e); }
|
|
721
|
+
});
|
|
722
|
+
try { window.dispatchEvent(new CustomEvent('craftsession', { detail: change })); } catch (e) {}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (SESSION && window.craft && !window.craft.session) {
|
|
726
|
+
window.craft.session = {
|
|
727
|
+
key: SESSION,
|
|
728
|
+
token: function () { return sessionToken(stored(SESSION)); },
|
|
729
|
+
onChange: function (callback) {
|
|
730
|
+
if (typeof callback !== 'function') throw new TypeError('craft.session.onChange needs a function');
|
|
731
|
+
sessionListeners.push(callback);
|
|
732
|
+
return function () {
|
|
733
|
+
var index = sessionListeners.indexOf(callback);
|
|
734
|
+
if (index >= 0) sessionListeners.splice(index, 1);
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
};
|
|
701
738
|
}
|
|
702
739
|
if (SHARE.length && window.Storage) {
|
|
703
740
|
var proto = window.Storage.prototype;
|
|
@@ -808,7 +845,9 @@ final class CraftHybrid: NSObject {
|
|
|
808
845
|
guard let nativeKey = config.shareStorage?[key] else { return }
|
|
809
846
|
// The page's own write, or its echo of one a native screen made.
|
|
810
847
|
pendingShared.removeValue(forKey: key)
|
|
811
|
-
|
|
848
|
+
// The page's sign-in or sign-out moves the account for every
|
|
849
|
+
// screen; an echo writes what is already there and moves nothing.
|
|
850
|
+
CraftSession.observe(key: nativeKey, config: config) { CraftHybridKeychain.set(nativeKey, value: value) }
|
|
812
851
|
case .navigated(let path, let direction, let depth, let byHost):
|
|
813
852
|
pagePath = path
|
|
814
853
|
pageDepth = depth
|
|
@@ -205,11 +205,21 @@ enum CraftNativeActions {
|
|
|
205
205
|
complete(requestToken, failure("CAPABILITY_DISABLED", "Secure storage is disabled"), completion)
|
|
206
206
|
return
|
|
207
207
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
let write = { () -> Result<Any, CraftNativeActionError> in
|
|
209
|
+
let result = secureStorage(method: method, args: args)
|
|
210
|
+
// A key the web page shares (`shareStorage`): the page gets it too,
|
|
211
|
+
// so a sign-in on a native screen signs the page in.
|
|
212
|
+
if case .success = result, ["set", "remove", "delete"].contains(method), let key = args.first as? String {
|
|
213
|
+
CraftHybrid.shared.nativeWroteShared(nativeKey: key, value: method == "set" ? args.count > 1 ? args[1] as? String : nil : nil)
|
|
214
|
+
}
|
|
215
|
+
return result
|
|
216
|
+
}
|
|
217
|
+
// A write that moves the sign-in token is the account's change:
|
|
218
|
+
// every screen hears it (CraftSession). Reads skip the check.
|
|
219
|
+
if ["set", "remove", "delete", "clear"].contains(method) {
|
|
220
|
+
sync = CraftSession.observe(key: method == "clear" ? nil : args.first as? String, config: config, write)
|
|
221
|
+
} else {
|
|
222
|
+
sync = write()
|
|
213
223
|
}
|
|
214
224
|
case ("Biometrics", "isAvailable"):
|
|
215
225
|
sync = config.enableBiometric ? .success(biometricContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)) : failure("CAPABILITY_DISABLED", "Biometric authentication is disabled")
|
|
@@ -1423,6 +1433,54 @@ extension CraftNativeActions {
|
|
|
1423
1433
|
}
|
|
1424
1434
|
}
|
|
1425
1435
|
|
|
1436
|
+
/// The signed-in account: the token kept in secure storage under
|
|
1437
|
+
/// `sessionTokenKey` (default `auth.token`). Whoever moves it, a native
|
|
1438
|
+
/// screen's `craft.secureStorage`, the page's own sign-in through a
|
|
1439
|
+
/// `shareStorage` key, or a sign-out's clear, posts `changed` once with
|
|
1440
|
+
/// `key`, `token` and `previous` (NSNull when signed out), after the Keychain
|
|
1441
|
+
/// holds the new value. Each native screen hands it to its script as
|
|
1442
|
+
/// `SESSION_CHANGED` and to `craft.session.onChange` (CraftNativeScreen).
|
|
1443
|
+
enum CraftSession {
|
|
1444
|
+
static let changed = Notification.Name("craftSessionChanged")
|
|
1445
|
+
static let defaultKey = "auth.token"
|
|
1446
|
+
/// One write and its before and after at a time, so two quick writes
|
|
1447
|
+
/// from different queues are two changes, never one reported twice.
|
|
1448
|
+
private static let lock = NSLock()
|
|
1449
|
+
|
|
1450
|
+
static func tokenKey(_ config: CraftConfig) -> String {
|
|
1451
|
+
guard let key = config.sessionTokenKey, !key.isEmpty else { return defaultKey }
|
|
1452
|
+
return key
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/// The token now, as `craft.secureStorage.getSync` reads it; nil when
|
|
1456
|
+
/// signed out (an empty token is no account either).
|
|
1457
|
+
static func token(_ config: CraftConfig) -> String? {
|
|
1458
|
+
guard config.enableSecureStorage,
|
|
1459
|
+
let value = CraftNativeActions.webSecureValue(forKey: tokenKey(config)), !value.isEmpty else { return nil }
|
|
1460
|
+
return value
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/// Run `write`, and post `changed` when it moved the token. `key` is the
|
|
1464
|
+
/// key written; nil for a clear, which can remove every key.
|
|
1465
|
+
static func observe<T>(key: String?, config: CraftConfig, _ write: () -> T) -> T {
|
|
1466
|
+
let name = tokenKey(config)
|
|
1467
|
+
guard key == nil || key == name else { return write() }
|
|
1468
|
+
lock.lock()
|
|
1469
|
+
let before = token(config)
|
|
1470
|
+
let result = write()
|
|
1471
|
+
let after = token(config)
|
|
1472
|
+
lock.unlock()
|
|
1473
|
+
if before != after { post(key: name, token: after, previous: before) }
|
|
1474
|
+
return result
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
static func post(key: String, token: String?, previous: String?) {
|
|
1478
|
+
let info: [String: Any] = ["key": key, "token": token ?? NSNull(), "previous": previous ?? NSNull()]
|
|
1479
|
+
let send = { NotificationCenter.default.post(name: changed, object: nil, userInfo: info) }
|
|
1480
|
+
if Thread.isMainThread { send() } else { DispatchQueue.main.async(execute: send) }
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1426
1484
|
/// Last-known data for a screen's first frame: one JSON file per name in
|
|
1427
1485
|
/// `Application Support/craft-snapshots/<name>.json`. A web page writes them
|
|
1428
1486
|
/// (`snapshots.set` in the mobile API) and a native screen reads them
|
|
@@ -4855,6 +4855,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4855
4855
|
let appURL = Self.jsonString(config.devServerURL ?? "")
|
|
4856
4856
|
let appVersion = Self.jsonString(info?["CFBundleShortVersionString"] as? String ?? "")
|
|
4857
4857
|
let appBuild = Self.jsonString(info?["CFBundleVersion"] as? String ?? "")
|
|
4858
|
+
let sessionTokenKey = Self.jsonString(CraftSession.tokenKey(config))
|
|
4858
4859
|
jsContext.evaluateScript("""
|
|
4859
4860
|
// JavaScriptCore has no DOM window, but browser-safe SDK bridges
|
|
4860
4861
|
// address the host through `window.craft`. Keep native screens
|
|
@@ -4873,6 +4874,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4873
4874
|
appURL: \(appURL),
|
|
4874
4875
|
appVersion: \(appVersion),
|
|
4875
4876
|
appBuild: \(appBuild),
|
|
4877
|
+
sessionTokenKey: \(sessionTokenKey),
|
|
4876
4878
|
postMessage: function(message) { craftNativePostMessage(message); },
|
|
4877
4879
|
onMessage: function(callback) { globalThis.__stxNativeCallback = callback; }
|
|
4878
4880
|
};
|
|
@@ -5274,6 +5276,33 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5274
5276
|
return value === undefined ? null : value;
|
|
5275
5277
|
}
|
|
5276
5278
|
});
|
|
5279
|
+
// The signed-in account (CraftSession): the token under
|
|
5280
|
+
// `sessionTokenKey`, and each change of it, which the host
|
|
5281
|
+
// reports through `__craftSessionChanged` after the Keychain
|
|
5282
|
+
// holds it, beside the `SESSION_CHANGED` bridge message.
|
|
5283
|
+
var sessionListeners = [];
|
|
5284
|
+
var sessionKey = globalThis.__stxNativeBridge && __stxNativeBridge.sessionTokenKey || 'auth.token';
|
|
5285
|
+
function sessionToken() {
|
|
5286
|
+
var value = craftNativeSecureGet(String(sessionKey));
|
|
5287
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
5288
|
+
}
|
|
5289
|
+
globalThis.__craftSessionChanged = function(token, previous) {
|
|
5290
|
+
sessionListeners.slice().forEach(function(callback) {
|
|
5291
|
+
try { callback(token, previous); } catch (error) { console.error('[session] ' + (error && error.stack || error)); }
|
|
5292
|
+
});
|
|
5293
|
+
};
|
|
5294
|
+
keep('session', {
|
|
5295
|
+
key: sessionKey,
|
|
5296
|
+
token: sessionToken,
|
|
5297
|
+
onChange: function(callback) {
|
|
5298
|
+
if (typeof callback !== 'function') throw new TypeError('craft.session.onChange needs a function');
|
|
5299
|
+
sessionListeners.push(callback);
|
|
5300
|
+
return function() {
|
|
5301
|
+
var index = sessionListeners.indexOf(callback);
|
|
5302
|
+
if (index >= 0) sessionListeners.splice(index, 1);
|
|
5303
|
+
};
|
|
5304
|
+
}
|
|
5305
|
+
});
|
|
5277
5306
|
keep('navigation', {
|
|
5278
5307
|
setOptions: function(options) { craftNativeSetOptions(JSON.stringify(options || {})); }
|
|
5279
5308
|
});
|
|
@@ -5739,6 +5768,22 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5739
5768
|
guard let name = notification.userInfo?["name"] as? String else { return }
|
|
5740
5769
|
self?.send(type: "SNAPSHOT_CHANGED", payload: ["name": name])
|
|
5741
5770
|
})
|
|
5771
|
+
if config.enableSecureStorage {
|
|
5772
|
+
// A sign-in, sign-out or another account, made on any screen or
|
|
5773
|
+
// the page (CraftSession): `SESSION_CHANGED { key, token,
|
|
5774
|
+
// previous }` for the runtime, then craft.session's listeners.
|
|
5775
|
+
lifecycleObservers.append(NotificationCenter.default.addObserver(
|
|
5776
|
+
forName: CraftSession.changed,
|
|
5777
|
+
object: nil,
|
|
5778
|
+
queue: .main
|
|
5779
|
+
) { [weak self] notification in
|
|
5780
|
+
guard let self, let info = notification.userInfo, let key = info["key"] as? String else { return }
|
|
5781
|
+
let token = info["token"] as? String
|
|
5782
|
+
let previous = info["previous"] as? String
|
|
5783
|
+
self.send(type: "SESSION_CHANGED", payload: ["key": key, "token": token ?? NSNull(), "previous": previous ?? NSNull()])
|
|
5784
|
+
self.jsContext.objectForKeyedSubscript("__craftSessionChanged")?.call(withArguments: [token ?? NSNull(), previous ?? NSNull()])
|
|
5785
|
+
})
|
|
5786
|
+
}
|
|
5742
5787
|
if config.enablePushNotifications {
|
|
5743
5788
|
// A new device token (iOS hands one out after a reinstall or a
|
|
5744
5789
|
// restore): every screen's `craft.push.onToken` hears it.
|