craft-native 0.0.146 → 0.0.148
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/templates/MainActivityNative.kt.template +33 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.js +1 -0
- package/dist/ios/templates/CraftApp.swift +56 -0
- package/dist/ios/templates/CraftHybrid.swift +26 -5
- package/dist/ios/templates/CraftNativeActions.swift +157 -1
- package/dist/ios/templates/CraftNativeScreen.swift +506 -39
- package/dist/ios/templates/CraftNativeServices.swift +567 -0
- package/package.json +1 -1
|
@@ -2094,6 +2094,21 @@ class MainActivity : AppCompatActivity() {
|
|
|
2094
2094
|
maxLines = if (multiline) props.optInt("numberOfLines", Int.MAX_VALUE).coerceAtLeast(1) else 1
|
|
2095
2095
|
isEnabled = props.optBoolean("editable", true)
|
|
2096
2096
|
configureTextStyle(this, style)
|
|
2097
|
+
// The style's padding is where the text starts, as on iOS and
|
|
2098
|
+
// the web; without one the field keeps its own.
|
|
2099
|
+
if (listOf("padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom").any { style.has(it) }) {
|
|
2100
|
+
val density = resources.displayMetrics.density
|
|
2101
|
+
val all = style.optDouble("padding", 0.0)
|
|
2102
|
+
val horizontal = style.optDouble("paddingHorizontal", all)
|
|
2103
|
+
val vertical = style.optDouble("paddingVertical", all)
|
|
2104
|
+
setPadding(
|
|
2105
|
+
(style.optDouble("paddingLeft", horizontal) * density).toInt(),
|
|
2106
|
+
(style.optDouble("paddingTop", vertical) * density).toInt(),
|
|
2107
|
+
(style.optDouble("paddingRight", horizontal) * density).toInt(),
|
|
2108
|
+
(style.optDouble("paddingBottom", vertical) * density).toInt(),
|
|
2109
|
+
)
|
|
2110
|
+
}
|
|
2111
|
+
autofillHintFor(props)?.let { setAutofillHints(it) }
|
|
2097
2112
|
val controlledValue = props.opt("value")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
2098
2113
|
if (controlledValue != null && text.toString() != controlledValue) {
|
|
2099
2114
|
screen.suppressedTextChanges.add(this)
|
|
@@ -2991,6 +3006,24 @@ class MainActivity : AppCompatActivity() {
|
|
|
2991
3006
|
return (if (keyboard == 0) base else keyboard) or capitalization or correction or secure
|
|
2992
3007
|
}
|
|
2993
3008
|
|
|
3009
|
+
/// The autofill hint for `textContentType` (iOS's names) or `autoComplete`
|
|
3010
|
+
/// (HTML's), so a password manager fills the field.
|
|
3011
|
+
private fun autofillHintFor(props: JSONObject): String? {
|
|
3012
|
+
val raw = props.optString("textContentType").ifBlank { props.optString("autoComplete") }
|
|
3013
|
+
return when (raw.replace("-", "").replace("_", "").lowercase()) {
|
|
3014
|
+
"username" -> View.AUTOFILL_HINT_USERNAME
|
|
3015
|
+
"password", "currentpassword" -> View.AUTOFILL_HINT_PASSWORD
|
|
3016
|
+
"newpassword" -> "newPassword"
|
|
3017
|
+
"onetimecode" -> "smsOTPCode"
|
|
3018
|
+
"emailaddress", "email" -> View.AUTOFILL_HINT_EMAIL_ADDRESS
|
|
3019
|
+
"telephonenumber", "tel", "phone" -> View.AUTOFILL_HINT_PHONE
|
|
3020
|
+
"name" -> View.AUTOFILL_HINT_NAME
|
|
3021
|
+
"postalcode" -> View.AUTOFILL_HINT_POSTAL_CODE
|
|
3022
|
+
"fullstreetaddress", "streetaddress" -> View.AUTOFILL_HINT_POSTAL_ADDRESS
|
|
3023
|
+
else -> null
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
|
|
2994
3027
|
private fun imeOptionsFor(props: JSONObject): Int = when (props.optString("returnKeyType")) {
|
|
2995
3028
|
"done" -> EditorInfo.IME_ACTION_DONE
|
|
2996
3029
|
"go" -> EditorInfo.IME_ACTION_GO
|
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.148";
|
|
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
package/dist/ios/src/index.js
CHANGED
|
@@ -789,6 +789,7 @@ async function init(options) {
|
|
|
789
789
|
cpSync(join(TEMPLATES_DIR, "CraftMusic.swift"), join(output, "Sources", "CraftMusic.swift"));
|
|
790
790
|
cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
|
|
791
791
|
cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
|
|
792
|
+
cpSync(join(TEMPLATES_DIR, "CraftNativeServices.swift"), join(output, "Sources", "CraftNativeServices.swift"));
|
|
792
793
|
cpSync(join(TEMPLATES_DIR, "CraftHybrid.swift"), join(output, "Sources", "CraftHybrid.swift"));
|
|
793
794
|
const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
794
795
|
const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{BACKGROUND_TASK_IDENTIFIERS\}\}/g, renderBackgroundTaskIdentifiers(config)).replace(/\{\{LAUNCH_SCREEN\}\}/g, renderLaunchScreen(config)).replace(/\{\{NON_EXEMPT_ENCRYPTION\}\}/g, config.usesNonExemptEncryption ? "true" : "false").replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
|
|
@@ -5174,6 +5174,8 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5174
5174
|
// MARK: - Callback Helpers
|
|
5175
5175
|
private func resolveCallback(_ callbackId: String?, result: Any) {
|
|
5176
5176
|
guard let id = callbackId else { return }
|
|
5177
|
+
// A native screen's call (CraftWebServices): answered in Swift.
|
|
5178
|
+
if CraftWebServices.resolve(id, result: result) { return }
|
|
5177
5179
|
let resultStr: String
|
|
5178
5180
|
do {
|
|
5179
5181
|
let data = try JSONSerialization.data(withJSONObject: result, options: [.fragmentsAllowed])
|
|
@@ -5196,6 +5198,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5196
5198
|
|
|
5197
5199
|
private func rejectCallback(_ callbackId: String?, error: String, code: String = "CRAFT_ERROR") {
|
|
5198
5200
|
guard let id = callbackId else { return }
|
|
5201
|
+
if CraftWebServices.reject(id, message: error, code: code) { return }
|
|
5199
5202
|
// A hand-off rejection goes back through Zig's error route, so the
|
|
5200
5203
|
// page's promise *rejects*. Delivering it as a result would run the
|
|
5201
5204
|
// app's then-branch with an error-shaped object — fabricated
|
|
@@ -7883,6 +7886,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7883
7886
|
|
|
7884
7887
|
// MARK: - Web Communication
|
|
7885
7888
|
private func sendToWeb(_ event: String, data: [String: Any]) {
|
|
7889
|
+
CraftWebServices.forward(event, data: data)
|
|
7886
7890
|
guard let webView = webView else { return }
|
|
7887
7891
|
do {
|
|
7888
7892
|
let jsonData = try JSONSerialization.data(withJSONObject: data)
|
|
@@ -8420,6 +8424,58 @@ final class CraftZigRuntime: NSObject {
|
|
|
8420
8424
|
}
|
|
8421
8425
|
}
|
|
8422
8426
|
|
|
8427
|
+
/// The page's services that live in the web view's coordinator, for native
|
|
8428
|
+
/// screens: GPS (one fix, a watch, and the route recording that keeps going
|
|
8429
|
+
/// in the background and survives a relaunch). A native screen asks with a
|
|
8430
|
+
/// callback id of the form "native:<uuid>", which the reply helpers answer
|
|
8431
|
+
/// here instead of in the page, and the coordinator's location events reach
|
|
8432
|
+
/// every native screen as well as the page. One recorder for both, so a run
|
|
8433
|
+
/// started on a page can be followed on a native screen.
|
|
8434
|
+
///
|
|
8435
|
+
/// The coordinator exists wherever the web view does: always in a hybrid app.
|
|
8436
|
+
/// Calls go to the Swift dispatcher; a build that links the Zig runtime keeps
|
|
8437
|
+
/// the page's calls on Zig.
|
|
8438
|
+
enum CraftWebServices {
|
|
8439
|
+
private static var waiting: [String: (Result<Any, CraftNativeActionError>) -> Void] = [:]
|
|
8440
|
+
/// Coordinator events a native screen hears too.
|
|
8441
|
+
static let forwardedEvents: Set<String> = ["craftLocationUpdate", "craftLocationError", "craftMusicChange"]
|
|
8442
|
+
static let event = Notification.Name("craftWebServicesEvent")
|
|
8443
|
+
|
|
8444
|
+
static var isAvailable: Bool { CraftSwiftShim.coordinator != nil }
|
|
8445
|
+
|
|
8446
|
+
static func perform(action: String, body: [String: Any], completion: @escaping (Result<Any, CraftNativeActionError>) -> Void) {
|
|
8447
|
+
DispatchQueue.main.async {
|
|
8448
|
+
guard let coordinator = CraftSwiftShim.coordinator else {
|
|
8449
|
+
completion(.failure(CraftNativeActionError(code: "CAPABILITY_DISABLED", message: "\(action) needs the app's web view, which this app does not have")))
|
|
8450
|
+
return
|
|
8451
|
+
}
|
|
8452
|
+
let id = "native:\(UUID().uuidString)"
|
|
8453
|
+
waiting[id] = completion
|
|
8454
|
+
coordinator.dispatch(action: action, body: body, callbackId: id)
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
8457
|
+
|
|
8458
|
+
/// True when `id` was a native screen's, which then has its answer.
|
|
8459
|
+
static func resolve(_ id: String, result: Any) -> Bool {
|
|
8460
|
+
guard id.hasPrefix("native:") else { return false }
|
|
8461
|
+
DispatchQueue.main.async { waiting.removeValue(forKey: id)?(.success(result)) }
|
|
8462
|
+
return true
|
|
8463
|
+
}
|
|
8464
|
+
|
|
8465
|
+
static func reject(_ id: String, message: String, code: String) -> Bool {
|
|
8466
|
+
guard id.hasPrefix("native:") else { return false }
|
|
8467
|
+
DispatchQueue.main.async { waiting.removeValue(forKey: id)?(.failure(CraftNativeActionError(code: code, message: message))) }
|
|
8468
|
+
return true
|
|
8469
|
+
}
|
|
8470
|
+
|
|
8471
|
+
static func forward(_ name: String, data: [String: Any]) {
|
|
8472
|
+
guard forwardedEvents.contains(name) else { return }
|
|
8473
|
+
DispatchQueue.main.async {
|
|
8474
|
+
NotificationCenter.default.post(name: event, object: nil, userInfo: ["event": name, "detail": data])
|
|
8475
|
+
}
|
|
8476
|
+
}
|
|
8477
|
+
}
|
|
8478
|
+
|
|
8423
8479
|
/// The seam the Zig dispatcher hands unmigrated actions through.
|
|
8424
8480
|
///
|
|
8425
8481
|
/// Discovery is symmetric and both directions are runtime-only. Zig finds this
|
|
@@ -538,8 +538,10 @@ final class CraftHybrid: NSObject {
|
|
|
538
538
|
|
|
539
539
|
// A shared key a native screen wrote (a sign-in there): into this
|
|
540
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
|
-
|
|
541
|
+
// a `craftsharedstorage` event for whoever holds it in memory. Named
|
|
542
|
+
// apart from the router's adopt: function declarations hoist, so a
|
|
543
|
+
// second `adopt` replaced the first and the router went unwrapped.
|
|
544
|
+
function adoptShared(values) {
|
|
543
545
|
Object.keys(values || {}).forEach(function (key) {
|
|
544
546
|
if (SHARE.indexOf(key) < 0) return;
|
|
545
547
|
var value = values[key];
|
|
@@ -555,7 +557,7 @@ final class CraftHybrid: NSObject {
|
|
|
555
557
|
|
|
556
558
|
window.__craftHybrid = {
|
|
557
559
|
match: function (url) { var path = local(url); return path === null ? null : match(path); },
|
|
558
|
-
adopt:
|
|
560
|
+
adopt: adoptShared,
|
|
559
561
|
// The shell's own navigation: never handed back to it, and without
|
|
560
562
|
// the page's slide, since the shell animates the push itself.
|
|
561
563
|
// Answers once the page has drawn `path` (its router settles before
|
|
@@ -911,11 +913,18 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
911
913
|
return nav
|
|
912
914
|
}
|
|
913
915
|
|
|
916
|
+
/// A native screen's route params for `path`: the pattern's and the
|
|
917
|
+
/// query's, and the path itself.
|
|
918
|
+
private func params(for path: String, _ match: CraftHybridRoutes.Match) -> [String: Any] {
|
|
919
|
+
var params: [String: Any] = match.params
|
|
920
|
+
params["path"] = path
|
|
921
|
+
return params
|
|
922
|
+
}
|
|
923
|
+
|
|
914
924
|
/// The entry for a path: its native screen, or a web entry.
|
|
915
925
|
private func entry(for path: String, isTabRoot: Bool, depth: Int?, params extra: [String: Any] = [:]) -> UIViewController {
|
|
916
926
|
if let match = hybrid.routes.match(path) {
|
|
917
|
-
var params
|
|
918
|
-
params["path"] = path
|
|
927
|
+
var params = self.params(for: path, match)
|
|
919
928
|
extra.forEach { params[$0.key] = $0.value }
|
|
920
929
|
let screen = CraftNativeScreenController(config: config, routeName: match.screen, routeParams: params)
|
|
921
930
|
screen.hybridEvents = self
|
|
@@ -928,6 +937,12 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
928
937
|
private func show(tab id: String, rootPath: String? = nil) {
|
|
929
938
|
cancelPendingTab()
|
|
930
939
|
let nav = stacks[id] ?? makeStack(tab: id, rootPath: rootPath ?? (id.isEmpty ? hybrid.startPath : id))
|
|
940
|
+
// A stack is hidden and shown rather than moved, so UIKit tells its
|
|
941
|
+
// screens nothing: the shell says which went away and which is back.
|
|
942
|
+
if id != currentTab {
|
|
943
|
+
(visibleStack?.topViewController as? CraftNativeScreenController)?.screenVisibilityChanged(false)
|
|
944
|
+
(nav.topViewController as? CraftNativeScreenController)?.screenVisibilityChanged(true)
|
|
945
|
+
}
|
|
931
946
|
currentTab = id
|
|
932
947
|
for (key, other) in stacks { other.view.isHidden = key != id }
|
|
933
948
|
view.bringSubviewToFront(nav.view)
|
|
@@ -994,6 +1009,8 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
994
1009
|
if let nav = stacks[tab], nav.viewControllers.count > 1 {
|
|
995
1010
|
nav.popToRootViewController(animated: !fromLink)
|
|
996
1011
|
}
|
|
1012
|
+
// `/m/store?coach=5`: the tab's own screen, with the query.
|
|
1013
|
+
(stacks[tab]?.viewControllers.first as? CraftNativeScreenController)?.receiveRouteParams(params(for: raw, match))
|
|
997
1014
|
return
|
|
998
1015
|
}
|
|
999
1016
|
if fromLink, let owner = CraftHybridRoutes.tab(owning: match.path, among: tabs), owner != currentTab {
|
|
@@ -1002,6 +1019,9 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1002
1019
|
guard let nav = visibleStack ?? stacks.values.first else { return }
|
|
1003
1020
|
if let existing = nav.viewControllers.last(where: { nativePaths[ObjectIdentifier($0)] == match.path }) {
|
|
1004
1021
|
if existing !== nav.topViewController { nav.popToViewController(existing, animated: true) }
|
|
1022
|
+
// Opened again, perhaps with another query (`?person=8`): the
|
|
1023
|
+
// screen hears the params it was opened with this time.
|
|
1024
|
+
(existing as? CraftNativeScreenController)?.receiveRouteParams(params(for: raw, match))
|
|
1005
1025
|
return
|
|
1006
1026
|
}
|
|
1007
1027
|
let screen = entry(for: raw, isTabRoot: false, depth: nil)
|
|
@@ -1066,6 +1086,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1066
1086
|
guard top is CraftHybridWebSlot else { return }
|
|
1067
1087
|
if let existing = nav.viewControllers.last(where: { nativePaths[ObjectIdentifier($0)] == match.path }) {
|
|
1068
1088
|
nav.popToViewController(existing, animated: direction != "pop")
|
|
1089
|
+
(existing as? CraftNativeScreenController)?.receiveRouteParams(params(for: path, match))
|
|
1069
1090
|
} else if !((top as? CraftHybridWebSlot)?.isTabRoot ?? false) {
|
|
1070
1091
|
nav.pushViewController(entry(for: path, isTabRoot: false, depth: nil), animated: true)
|
|
1071
1092
|
}
|
|
@@ -38,7 +38,9 @@ enum CraftNativeActions {
|
|
|
38
38
|
private static let sqliteTransient = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
|
|
39
39
|
|
|
40
40
|
static func capabilities(config: CraftConfig) -> [String] {
|
|
41
|
-
|
|
41
|
+
// `speech`, `audio` (background sound) and `eventStream` (a fetch read
|
|
42
|
+
// as it arrives) need nothing the app has to declare.
|
|
43
|
+
var values = ["device", "clipboard", "haptics", "storage", "lifecycle", "fetch", "dialog", "speech", "audio", "eventStream", "linking"]
|
|
42
44
|
if config.enableBiometric { values.append("biometric") }
|
|
43
45
|
if config.enableSecureStorage { values.append("secureStorage") }
|
|
44
46
|
if config.enableLocalDatabase { values.append("database") }
|
|
@@ -48,6 +50,17 @@ enum CraftNativeActions {
|
|
|
48
50
|
// The in-app browser and the system's web sign-in sheet (CraftBrowser).
|
|
49
51
|
values.append("browser")
|
|
50
52
|
if config.enableSocialAuth { values.append("appleSignIn") }
|
|
53
|
+
if config.enablePushNotifications { values.append("push") }
|
|
54
|
+
if config.enableShare { values.append("share") }
|
|
55
|
+
// The screen's brightness and the status bar's style need nothing
|
|
56
|
+
// declared; keeping the screen awake is the app's choice.
|
|
57
|
+
values.append(contentsOf: ["brightness", "statusBar"])
|
|
58
|
+
if config.enableKeepAwake { values.append("keepAwake") }
|
|
59
|
+
// GPS and the route recording are the web view's coordinator's
|
|
60
|
+
// (CraftWebServices), so a native-only app, which has none, has none.
|
|
61
|
+
if config.enableGeolocation && config.renderer != "native" { values.append("geolocation") }
|
|
62
|
+
// The Music app's song and transport are the coordinator's too.
|
|
63
|
+
if config.enableAppleMusic && config.renderer != "native" { values.append("music") }
|
|
51
64
|
return values
|
|
52
65
|
}
|
|
53
66
|
|
|
@@ -207,6 +220,149 @@ enum CraftNativeActions {
|
|
|
207
220
|
complete(requestToken, result.mapError { CraftNativeActionError(code: $0.code, message: $0.message) }, completion)
|
|
208
221
|
}
|
|
209
222
|
return
|
|
223
|
+
case ("KeepAwake", "set"):
|
|
224
|
+
guard config.enableKeepAwake else {
|
|
225
|
+
complete(requestToken, failure("CAPABILITY_DISABLED", "Keep awake is disabled"), completion)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
let enabled = args.first as? Bool ?? true
|
|
229
|
+
DispatchQueue.main.async {
|
|
230
|
+
UIApplication.shared.isIdleTimerDisabled = enabled
|
|
231
|
+
complete(requestToken, .success(enabled), completion)
|
|
232
|
+
}
|
|
233
|
+
return
|
|
234
|
+
case ("Brightness", "get"):
|
|
235
|
+
DispatchQueue.main.async { complete(requestToken, .success(Double(UIScreen.main.brightness)), completion) }
|
|
236
|
+
return
|
|
237
|
+
case ("Brightness", "set"):
|
|
238
|
+
// The device's backlight, system-wide: a screen that dims it reads
|
|
239
|
+
// it first and puts it back, as @stacksjs/mobile's brightness does.
|
|
240
|
+
guard let level = (args.first as? NSNumber)?.doubleValue, level.isFinite else {
|
|
241
|
+
complete(requestToken, failure("INVALID_ARGUMENT", "Brightness.set needs a level from 0 to 1"), completion)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
DispatchQueue.main.async {
|
|
245
|
+
UIScreen.main.brightness = CGFloat(min(max(level, 0), 1))
|
|
246
|
+
complete(requestToken, .success(Double(UIScreen.main.brightness)), completion)
|
|
247
|
+
}
|
|
248
|
+
return
|
|
249
|
+
case ("StatusBar", "setStyle"):
|
|
250
|
+
let style = args.first as? String ?? "default"
|
|
251
|
+
DispatchQueue.main.async {
|
|
252
|
+
complete(requestToken, .success(CraftStatusBar.setStyle(style == "light" || style == "dark" ? style : "default")), completion)
|
|
253
|
+
}
|
|
254
|
+
return
|
|
255
|
+
case ("Location", _):
|
|
256
|
+
// The page's own GPS and route recorder, in the web view's
|
|
257
|
+
// coordinator: one recorder, which keeps recording in the
|
|
258
|
+
// background and survives a relaunch, whichever side started it.
|
|
259
|
+
guard config.enableGeolocation else {
|
|
260
|
+
complete(requestToken, failure("CAPABILITY_DISABLED", "Geolocation is disabled"), completion)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
let actions = [
|
|
264
|
+
"getCurrentPosition": "getCurrentPosition", "watchPosition": "watchPosition", "clearWatch": "clearWatch",
|
|
265
|
+
"startRecording": "startLocationRecording", "pauseRecording": "pauseLocationRecording",
|
|
266
|
+
"resumeRecording": "resumeLocationRecording", "stopRecording": "stopLocationRecording",
|
|
267
|
+
"getRecordingState": "getLocationRecordingState", "readRecording": "readLocationRecording",
|
|
268
|
+
]
|
|
269
|
+
guard let action = actions[method] else {
|
|
270
|
+
complete(requestToken, failure("UNKNOWN_ACTION", "Unsupported Location method"), completion)
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
CraftWebServices.perform(action: action, body: args.first as? [String: Any] ?? [:]) { complete(requestToken, $0, completion) }
|
|
274
|
+
return
|
|
275
|
+
case ("Music", _):
|
|
276
|
+
// The page's own Music app bridge (CraftMusic.swift), in the web
|
|
277
|
+
// view's coordinator: one observer, whose changes reach the page
|
|
278
|
+
// and every native screen as `craftMusicChange`.
|
|
279
|
+
guard config.enableAppleMusic else {
|
|
280
|
+
complete(requestToken, failure("CAPABILITY_DISABLED", "Apple Music is disabled"), completion)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
let actions = [
|
|
284
|
+
"getAuthorizationStatus": "musicGetAuthorization", "requestAuthorization": "musicRequestAuthorization",
|
|
285
|
+
"getNowPlaying": "musicGetNowPlaying", "control": "musicControl", "observe": "musicObserve",
|
|
286
|
+
]
|
|
287
|
+
guard let action = actions[method] else {
|
|
288
|
+
complete(requestToken, failure("UNKNOWN_ACTION", "Unsupported Music method"), completion)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
CraftWebServices.perform(action: action, body: args.first as? [String: Any] ?? [:]) { complete(requestToken, $0, completion) }
|
|
292
|
+
return
|
|
293
|
+
case ("Linking", "openURL"), ("Linking", "canOpenURL"):
|
|
294
|
+
// Another app or the system: `webcal:` subscriptions, `mailto:`,
|
|
295
|
+
// `tel:`, the App Store, this app's page in Settings
|
|
296
|
+
// (`UIApplication.openSettingsURLString`, which is `app-settings:`).
|
|
297
|
+
guard let address = args.first as? String, let url = URL(string: address), url.scheme != nil else {
|
|
298
|
+
complete(requestToken, failure("INVALID_ARGUMENT", "Linking.\(method) needs a URL it can parse"), completion)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
DispatchQueue.main.async {
|
|
302
|
+
if method == "canOpenURL" {
|
|
303
|
+
complete(requestToken, .success(UIApplication.shared.canOpenURL(url)), completion)
|
|
304
|
+
} else {
|
|
305
|
+
UIApplication.shared.open(url) { complete(requestToken, .success($0), completion) }
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return
|
|
309
|
+
case ("Share", "share"):
|
|
310
|
+
guard config.enableShare else {
|
|
311
|
+
complete(requestToken, failure("CAPABILITY_DISABLED", "Sharing is disabled"), completion)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
let options = args.first as? [String: Any] ?? [:]
|
|
315
|
+
DispatchQueue.main.async {
|
|
316
|
+
CraftNativeShare.share(options) { complete(requestToken, $0, completion) }
|
|
317
|
+
}
|
|
318
|
+
return
|
|
319
|
+
case ("Speech", "speak"):
|
|
320
|
+
// Settles when the words have been said (true) or were cut off
|
|
321
|
+
// (false), as the page's `craft.speech.speak` does.
|
|
322
|
+
guard let text = args.first as? String, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
323
|
+
complete(requestToken, failure("INVALID_ARGUMENT", "speech.speak needs some text to say"), completion)
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
let options = args.count > 1 ? args[1] as? [String: Any] ?? [:] : [:]
|
|
327
|
+
DispatchQueue.main.async {
|
|
328
|
+
CraftNativeSpeech.shared.speak(
|
|
329
|
+
text,
|
|
330
|
+
language: options["language"] as? String,
|
|
331
|
+
rate: (options["rate"] as? NSNumber)?.doubleValue,
|
|
332
|
+
interrupt: options["interrupt"] as? Bool ?? true
|
|
333
|
+
) { spoken in complete(requestToken, .success(spoken), completion) }
|
|
334
|
+
}
|
|
335
|
+
return
|
|
336
|
+
case ("Speech", "stop"):
|
|
337
|
+
DispatchQueue.main.async {
|
|
338
|
+
CraftNativeSpeech.shared.stop()
|
|
339
|
+
complete(requestToken, .success(true), completion)
|
|
340
|
+
}
|
|
341
|
+
return
|
|
342
|
+
case ("Push", _):
|
|
343
|
+
guard config.enablePushNotifications else {
|
|
344
|
+
complete(requestToken, failure("CAPABILITY_DISABLED", "Push notifications are disabled"), completion)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
switch method {
|
|
348
|
+
case "getPermissionStatus":
|
|
349
|
+
CraftNativePush.status { complete(requestToken, .success($0), completion) }
|
|
350
|
+
case "requestPermission":
|
|
351
|
+
CraftNativePush.requestPermission { complete(requestToken, .success($0), completion) }
|
|
352
|
+
case "register":
|
|
353
|
+
CraftNativePush.register { complete(requestToken, $0, completion) }
|
|
354
|
+
case "openSettings":
|
|
355
|
+
DispatchQueue.main.async {
|
|
356
|
+
guard let url = URL(string: UIApplication.openSettingsURLString) else {
|
|
357
|
+
complete(requestToken, .success(false), completion)
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
UIApplication.shared.open(url) { complete(requestToken, .success($0), completion) }
|
|
361
|
+
}
|
|
362
|
+
default:
|
|
363
|
+
complete(requestToken, failure("UNKNOWN_ACTION", "Unsupported Push method"), completion)
|
|
364
|
+
}
|
|
365
|
+
return
|
|
210
366
|
case ("Notifications", _):
|
|
211
367
|
guard config.enableLocalNotifications else {
|
|
212
368
|
complete(requestToken, failure("CAPABILITY_DISABLED", "Local notifications are disabled"), completion)
|