craft-native 0.0.486 → 0.0.490
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/android/src/index.d.ts +28 -0
- package/dist/android/src/index.js +15 -0
- package/dist/android/templates/CraftNativeServices.kt.template +10 -1
- package/dist/android/templates/CraftNativeShell.kt.template +96 -8
- package/dist/android/templates/MainActivityNative.kt.template +145 -30
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +42 -1
- package/dist/ios/src/index.js +15 -0
- package/dist/ios/templates/CraftApp.swift +30 -0
- package/dist/ios/templates/CraftHybrid.swift +49 -0
- package/dist/ios/templates/CraftNativeActions.swift +9 -1
- package/dist/ios/templates/CraftNativeScreen.swift +353 -95
- package/dist/mobile.js +2 -2
- package/package.json +1 -1
|
@@ -2046,6 +2046,9 @@ struct CraftConfig: Codable {
|
|
|
2046
2046
|
/// (`auth.token` unless set): `craft.session` follows it, and a change of
|
|
2047
2047
|
/// it is `SESSION_CHANGED` on every screen (CraftSession).
|
|
2048
2048
|
var sessionTokenKey: String? = nil
|
|
2049
|
+
/// Which screens a signed-out launch may start on, resolved before the
|
|
2050
|
+
/// first frame is built (CraftHybrid.configure).
|
|
2051
|
+
var nativeGate: NativeGate? = nil
|
|
2049
2052
|
/// The bundle route of the app script: a screen-less script that runs for
|
|
2050
2053
|
/// the app's whole life and hears the app's events (CraftHybrid.swift).
|
|
2051
2054
|
var nativeApp: String? = nil
|
|
@@ -2057,6 +2060,33 @@ struct CraftConfig: Codable {
|
|
|
2057
2060
|
/// unless the config names a project key.
|
|
2058
2061
|
var crashReporting: CrashReporting? = nil
|
|
2059
2062
|
|
|
2063
|
+
/// An auth gate the shell applies while resolving the start path.
|
|
2064
|
+
///
|
|
2065
|
+
/// An app script cannot do this. It is started from the first frame's
|
|
2066
|
+
/// `CATransaction` completion block, so by the time it can read a token
|
|
2067
|
+
/// the signed-in screen it would have prevented is already on screen —
|
|
2068
|
+
/// with the previous session's kept snapshot in it when the launch
|
|
2069
|
+
/// follows an expired token rather than a sign-out (#499). The check has
|
|
2070
|
+
/// to belong to whoever decides the start path, and that is the shell.
|
|
2071
|
+
struct NativeGate: Codable {
|
|
2072
|
+
/// Paths that may be shown without a sign-in. A path is public when
|
|
2073
|
+
/// it is listed, or when it sits under a listed one: `/m/store`
|
|
2074
|
+
/// admits `/m/store/42` and not `/m/storefront`.
|
|
2075
|
+
///
|
|
2076
|
+
/// Spelled `publicPaths` rather than `public` in the stored config
|
|
2077
|
+
/// too. `public` is a Swift keyword, and remapping it through
|
|
2078
|
+
/// `CodingKeys` would make the property name and the stored key
|
|
2079
|
+
/// differ — which is exactly what the Zig conformance mirror checks,
|
|
2080
|
+
/// and a divergence worth not having for the sake of one word.
|
|
2081
|
+
var publicPaths: [String]? = nil
|
|
2082
|
+
/// Where a signed-out launch starts instead.
|
|
2083
|
+
var redirect: String? = nil
|
|
2084
|
+
/// The secure-storage key holding the sign-in. Defaults to
|
|
2085
|
+
/// `sessionTokenKey`, which is the same item `craft.session` follows,
|
|
2086
|
+
/// so an app that already declares one need not repeat it.
|
|
2087
|
+
var token: String? = nil
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2060
2090
|
struct Tab: Codable {
|
|
2061
2091
|
var id: String
|
|
2062
2092
|
var title: String
|
|
@@ -425,6 +425,7 @@ final class CraftHybrid: NSObject {
|
|
|
425
425
|
: "[craft hybrid] nativeScreens are set but dist/native-screen.js is missing; the app has no screens to show")
|
|
426
426
|
}
|
|
427
427
|
isEnabled = wanted && Self.hasBundle
|
|
428
|
+
applyGate(config)
|
|
428
429
|
if isEnabled, !hasWeb, routes.match(startPath) == nil {
|
|
429
430
|
// No web view to show it in: the first tab a screen draws.
|
|
430
431
|
let fallback = (config.tabs ?? []).map(\.id).first { routes.match($0) != nil }
|
|
@@ -458,6 +459,54 @@ final class CraftHybrid: NSObject {
|
|
|
458
459
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { self.startAppScript() }
|
|
459
460
|
}
|
|
460
461
|
|
|
462
|
+
/// Whether `path` may be shown to someone who is not signed in.
|
|
463
|
+
///
|
|
464
|
+
/// Listed, or under something listed: `/m/store` admits `/m/store/42` and
|
|
465
|
+
/// not `/m/storefront`. Prefixes are compared with the separator attached
|
|
466
|
+
/// so a listed path cannot admit a sibling that merely starts with its
|
|
467
|
+
/// letters.
|
|
468
|
+
static func gateAdmits(_ path: String, public allowed: [String]) -> Bool {
|
|
469
|
+
allowed.contains { entry in
|
|
470
|
+
var listed = entry
|
|
471
|
+
while listed.count > 1, listed.hasSuffix("/") { listed.removeLast() }
|
|
472
|
+
guard !listed.isEmpty else { return false }
|
|
473
|
+
if path == listed { return true }
|
|
474
|
+
// "/" is the root path, not a prefix of everything. Reading it as
|
|
475
|
+
// one would admit every path, which is a gate that is not a gate.
|
|
476
|
+
guard listed != "/" else { return false }
|
|
477
|
+
return path.hasPrefix(listed + "/")
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/// Send a signed-out launch to the gate's redirect before anything is built.
|
|
482
|
+
///
|
|
483
|
+
/// This runs while `startPath` is still being resolved, which is the
|
|
484
|
+
/// whole point: the app script that would otherwise do it does not start
|
|
485
|
+
/// until the first frame has been committed, so it can only push sign-in
|
|
486
|
+
/// on top of a screen that has already been drawn with the previous
|
|
487
|
+
/// session's data in it (#499).
|
|
488
|
+
///
|
|
489
|
+
/// The Keychain read is synchronous and can cost 100-900ms cold. It is
|
|
490
|
+
/// not an extra read: every signed-in screen reads this same item when it
|
|
491
|
+
/// opens, and `CraftKeychain` caches it, so the gate moves the app's first
|
|
492
|
+
/// read earlier rather than adding one. Only apps that declare a gate pay
|
|
493
|
+
/// it at all.
|
|
494
|
+
private func applyGate(_ config: CraftConfig) {
|
|
495
|
+
guard let gate = config.nativeGate, let redirect = gate.redirect, !redirect.isEmpty else { return }
|
|
496
|
+
guard !Self.gateAdmits(startPath, public: gate.publicPaths ?? []) else { return }
|
|
497
|
+
let key = gate.token ?? config.sessionTokenKey ?? "auth.token"
|
|
498
|
+
guard (CraftNativeActions.webSecureValue(forKey: key) ?? "").isEmpty else { return }
|
|
499
|
+
// A redirect nothing can draw would trade a private screen for a
|
|
500
|
+
// blank one. Say so and leave the start path alone; the fallback
|
|
501
|
+
// below then treats it as any other unroutable start.
|
|
502
|
+
if isEnabled, !hasWeb, routes.match(redirect) == nil {
|
|
503
|
+
NSLog("[craft hybrid] nativeGate redirect %@ has no native screen; the gate did not run", redirect)
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
NSLog("[craft hybrid] no sign-in under %@: starting on %@ instead of %@", key, redirect, startPath)
|
|
507
|
+
startPath = redirect
|
|
508
|
+
}
|
|
509
|
+
|
|
461
510
|
/// A silent push or a background refresh is waiting for the app script,
|
|
462
511
|
/// with iOS's clock running: it starts now rather than after a frame,
|
|
463
512
|
/// which a launch in the background never draws.
|
|
@@ -2001,7 +2001,15 @@ enum CraftSnapshots {
|
|
|
2001
2001
|
}
|
|
2002
2002
|
lock.unlock()
|
|
2003
2003
|
DispatchQueue.main.async {
|
|
2004
|
-
|
|
2004
|
+
// Posted whether or not the write landed. A screen displays its
|
|
2005
|
+
// checkpoint optimistically, and this notification carries only
|
|
2006
|
+
// the name, so a reader answers it by calling `read` — which
|
|
2007
|
+
// after a failure finds the previous value, because `pending`
|
|
2008
|
+
// was cleared above and the cache was only refilled on success.
|
|
2009
|
+
// Posting only on success left a failed checkpoint on screen as
|
|
2010
|
+
// though it had been kept; Android already restores it
|
|
2011
|
+
// (`changed("snapshots", name, if (ok) json else previous, null)`).
|
|
2012
|
+
NotificationCenter.default.post(name: changed, object: nil, userInfo: ["name": name])
|
|
2005
2013
|
completion?(ok)
|
|
2006
2014
|
}
|
|
2007
2015
|
}
|