@craft-native/ios 0.0.99 → 0.0.101
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 +2 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -1
- package/package.json +1 -1
- package/templates/CraftApp.swift +106 -10
package/dist/cli.js
CHANGED
|
@@ -390,12 +390,13 @@ async function init(options) {
|
|
|
390
390
|
}
|
|
391
391
|
const finalBundleId = bundleId || `com.craft.${name.toLowerCase()}`;
|
|
392
392
|
const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
|
|
393
|
+
const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
|
|
393
394
|
const config = {
|
|
394
395
|
...DEFAULT_CONFIG,
|
|
395
396
|
appName: displayName,
|
|
396
397
|
bundleId: finalBundleId,
|
|
397
398
|
teamId: teamId || "",
|
|
398
|
-
...
|
|
399
|
+
...given
|
|
399
400
|
};
|
|
400
401
|
if (config.enableBackgroundLocation)
|
|
401
402
|
config.enableGeolocation = true;
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface CraftConfig {
|
|
|
19
19
|
backgroundColor?: string;
|
|
20
20
|
/** The launch and webview background while the phone is in Dark Mode. */
|
|
21
21
|
backgroundColorDark?: string;
|
|
22
|
+
/** Edge-swipe back and forward through the page's history, pushState entries included. */
|
|
23
|
+
swipeNavigation?: boolean;
|
|
22
24
|
enableSpeechRecognition?: boolean;
|
|
23
25
|
enableHaptics?: boolean;
|
|
24
26
|
enableShare?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -385,12 +385,13 @@ async function init(options) {
|
|
|
385
385
|
}
|
|
386
386
|
const finalBundleId = bundleId || `com.craft.${name.toLowerCase()}`;
|
|
387
387
|
const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
|
|
388
|
+
const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
|
|
388
389
|
const config = {
|
|
389
390
|
...DEFAULT_CONFIG,
|
|
390
391
|
appName: displayName,
|
|
391
392
|
bundleId: finalBundleId,
|
|
392
393
|
teamId: teamId || "",
|
|
393
|
-
...
|
|
394
|
+
...given
|
|
394
395
|
};
|
|
395
396
|
if (config.enableBackgroundLocation)
|
|
396
397
|
config.enableGeolocation = true;
|
package/package.json
CHANGED
package/templates/CraftApp.swift
CHANGED
|
@@ -337,17 +337,40 @@ class AppState: ObservableObject {
|
|
|
337
337
|
@Published var config: CraftConfig
|
|
338
338
|
|
|
339
339
|
init() {
|
|
340
|
-
// Load config from craft.config.json if available
|
|
341
340
|
if let configURL = Bundle.main.url(forResource: "craft.config", withExtension: "json"),
|
|
342
|
-
let data = try? Data(contentsOf: configURL)
|
|
343
|
-
|
|
344
|
-
self.config = config
|
|
341
|
+
let data = try? Data(contentsOf: configURL) {
|
|
342
|
+
self.config = CraftConfig.load(from: data)
|
|
345
343
|
} else {
|
|
346
344
|
self.config = CraftConfig()
|
|
347
345
|
}
|
|
348
346
|
}
|
|
349
347
|
}
|
|
350
348
|
|
|
349
|
+
extension CraftConfig {
|
|
350
|
+
/// The bundled config laid over the defaults, key by key.
|
|
351
|
+
///
|
|
352
|
+
/// Decoded whole, one missing or mistyped key failed the entire file and
|
|
353
|
+
/// the app ran on `CraftConfig()` — every capability off, silently: the
|
|
354
|
+
/// network bridge refused, the page read that as offline, and nothing said
|
|
355
|
+
/// why. A key the file leaves out now keeps its default, and a file that
|
|
356
|
+
/// still cannot be read says so in the log.
|
|
357
|
+
static func load(from data: Data) -> CraftConfig {
|
|
358
|
+
let defaults = (try? JSONEncoder().encode(CraftConfig()))
|
|
359
|
+
.flatMap { try? JSONSerialization.jsonObject(with: $0) as? [String: Any] } ?? [:]
|
|
360
|
+
guard let given = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
|
|
361
|
+
NSLog("[craft] craft.config.json is not a JSON object; running on defaults")
|
|
362
|
+
return CraftConfig()
|
|
363
|
+
}
|
|
364
|
+
let merged = defaults.merging(given.filter { !($0.value is NSNull) }) { _, bundled in bundled }
|
|
365
|
+
do {
|
|
366
|
+
return try JSONDecoder().decode(CraftConfig.self, from: JSONSerialization.data(withJSONObject: merged))
|
|
367
|
+
} catch {
|
|
368
|
+
NSLog("[craft] craft.config.json could not be read, running on defaults: %@", String(describing: error))
|
|
369
|
+
return CraftConfig()
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
351
374
|
// MARK: - Configuration
|
|
352
375
|
struct CraftConfig: Codable {
|
|
353
376
|
var appName: String = "Craft App"
|
|
@@ -359,6 +382,8 @@ struct CraftConfig: Codable {
|
|
|
359
382
|
var backgroundColor: String = "#1a1a2e"
|
|
360
383
|
/// The background while the phone is in Dark Mode; the light one if unset.
|
|
361
384
|
var backgroundColorDark: String? = nil
|
|
385
|
+
/// Edge-swipe back and forward through the page's history.
|
|
386
|
+
var swipeNavigation: Bool? = nil
|
|
362
387
|
var enableSpeechRecognition: Bool = false
|
|
363
388
|
var enableHaptics: Bool = false
|
|
364
389
|
var enableShare: Bool = false
|
|
@@ -512,6 +537,48 @@ final class BundledAssetSchemeHandler: NSObject, WKURLSchemeHandler {
|
|
|
512
537
|
}
|
|
513
538
|
}
|
|
514
539
|
|
|
540
|
+
#if DEBUG
|
|
541
|
+
/// Relays the page's console and uncaught errors to NSLog in debug builds.
|
|
542
|
+
final class PageConsoleRelay: NSObject, WKScriptMessageHandler {
|
|
543
|
+
static let script = """
|
|
544
|
+
(function() {
|
|
545
|
+
var handler = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.craftLog;
|
|
546
|
+
if (!handler) return;
|
|
547
|
+
function text(value) {
|
|
548
|
+
try { return typeof value === 'string' ? value : (value && value.stack) || JSON.stringify(value); }
|
|
549
|
+
catch (e) { return String(value); }
|
|
550
|
+
}
|
|
551
|
+
function send(level, message) { try { handler.postMessage({ level: level, message: String(message).slice(0, 4000) }); } catch (e) {} }
|
|
552
|
+
['error', 'warn', 'log', 'info'].forEach(function(level) {
|
|
553
|
+
var original = console[level];
|
|
554
|
+
console[level] = function() {
|
|
555
|
+
send(level, Array.prototype.map.call(arguments, text).join(' '));
|
|
556
|
+
return original.apply(console, arguments);
|
|
557
|
+
};
|
|
558
|
+
});
|
|
559
|
+
// Capture phase: a script or stylesheet that fails to load fires its
|
|
560
|
+
// error on the element, which never bubbles to window.
|
|
561
|
+
window.addEventListener('error', function(event) {
|
|
562
|
+
var target = event.target;
|
|
563
|
+
if (target && target !== window && (target.src || target.href)) {
|
|
564
|
+
send('error', 'failed to load ' + (target.src || target.href));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
send('error', 'uncaught ' + event.message + ' at ' + event.filename + ':' + event.lineno + ':' + event.colno);
|
|
568
|
+
}, true);
|
|
569
|
+
window.addEventListener('unhandledrejection', function(event) {
|
|
570
|
+
send('error', 'unhandled rejection ' + text(event.reason));
|
|
571
|
+
});
|
|
572
|
+
})();
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
576
|
+
guard let body = message.body as? [String: Any] else { return }
|
|
577
|
+
NSLog("[craft page] %@: %@", body["level"] as? String ?? "log", body["message"] as? String ?? "")
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
#endif
|
|
581
|
+
|
|
515
582
|
struct CraftWebView: UIViewRepresentable {
|
|
516
583
|
let config: CraftConfig
|
|
517
584
|
|
|
@@ -526,10 +593,25 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
526
593
|
let contentController = WKUserContentController()
|
|
527
594
|
contentController.add(context.coordinator, name: "craft")
|
|
528
595
|
webConfig.userContentController = contentController
|
|
596
|
+
#if DEBUG
|
|
597
|
+
// The page's errors and console, in the device log, so a debug build
|
|
598
|
+
// can be diagnosed from `log stream` or `simctl spawn … log show`
|
|
599
|
+
// without attaching Safari. A separate handler, outside the bridge's
|
|
600
|
+
// action dispatch. Never in a release build.
|
|
601
|
+
contentController.add(PageConsoleRelay(), name: "craftLog")
|
|
602
|
+
contentController.addUserScript(WKUserScript(source: PageConsoleRelay.script, injectionTime: .atDocumentStart, forMainFrameOnly: true))
|
|
603
|
+
#endif
|
|
529
604
|
|
|
530
605
|
let webView = WKWebView(frame: .zero, configuration: webConfig)
|
|
531
606
|
webView.navigationDelegate = context.coordinator
|
|
532
607
|
webView.isOpaque = false
|
|
608
|
+
// An edge swipe goes back (and forward) through the page's history,
|
|
609
|
+
// pushState entries included, the way every iOS app's stack does.
|
|
610
|
+
webView.allowsBackForwardNavigationGestures = config.swipeNavigation ?? false
|
|
611
|
+
#if DEBUG
|
|
612
|
+
// Safari's Develop menu can attach to a debug build; never a release.
|
|
613
|
+
if #available(iOS 16.4, *) { webView.isInspectable = true }
|
|
614
|
+
#endif
|
|
533
615
|
|
|
534
616
|
// Before any message can be offered. Zig evaluates every reply against
|
|
535
617
|
// this webview, and without it an action would run, succeed, and reach
|
|
@@ -1301,7 +1383,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
1301
1383
|
case "requestHealthAuthorization":
|
|
1302
1384
|
if config.enableHealthKit {
|
|
1303
1385
|
let types = body["types"] as? [String] ?? []
|
|
1304
|
-
requestHealthAuthorization(types: types, callbackId: callbackId)
|
|
1386
|
+
requestHealthAuthorization(types: types, readOnly: body["readOnly"] as? Bool ?? false, callbackId: callbackId)
|
|
1305
1387
|
} else {
|
|
1306
1388
|
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
1307
1389
|
}
|
|
@@ -3239,19 +3321,28 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3239
3321
|
var legacyNotifications = craft.notifications;
|
|
3240
3322
|
|
|
3241
3323
|
craft.contractVersion = '1.0.0';
|
|
3324
|
+
// Actions answered by a person, through a system sheet they
|
|
3325
|
+
// may take a minute to read. Thirty seconds of reading the
|
|
3326
|
+
// HealthKit sheet used to fail the call while the sheet was
|
|
3327
|
+
// still open; these wait for the answer instead.
|
|
3328
|
+
var personFacing = {
|
|
3329
|
+
requestHealthAuthorization: true,
|
|
3330
|
+
requestPermission: true,
|
|
3331
|
+
signInWithApple: true
|
|
3332
|
+
};
|
|
3242
3333
|
craft._invoke = function(action, payload) {
|
|
3243
3334
|
var self = craft;
|
|
3244
3335
|
var id = 'cb_' + (++self._callbackId);
|
|
3245
3336
|
var message = Object.assign({}, payload || {}, {action: action, callbackId: id});
|
|
3246
3337
|
window.webkit.messageHandlers.craft.postMessage(message);
|
|
3247
3338
|
return new Promise(function(resolve, reject) {
|
|
3248
|
-
var timeout = setTimeout(function() {
|
|
3339
|
+
var timeout = personFacing[action] ? null : setTimeout(function() {
|
|
3249
3340
|
delete self._callbacks[id];
|
|
3250
3341
|
reject(new Error('Craft bridge timed out: ' + action));
|
|
3251
3342
|
}, 30000);
|
|
3252
3343
|
self._callbacks[id] = {
|
|
3253
|
-
resolve: function(value) { clearTimeout(timeout); resolve(value); },
|
|
3254
|
-
reject: function(error) { clearTimeout(timeout); reject(error); }
|
|
3344
|
+
resolve: function(value) { if (timeout) clearTimeout(timeout); resolve(value); },
|
|
3345
|
+
reject: function(error) { if (timeout) clearTimeout(timeout); reject(error); }
|
|
3255
3346
|
};
|
|
3256
3347
|
});
|
|
3257
3348
|
};
|
|
@@ -3387,7 +3478,9 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
3387
3478
|
readRecording: function() { return craft._invoke('readLocationRecording'); }
|
|
3388
3479
|
};
|
|
3389
3480
|
craft.health = {
|
|
3390
|
-
requestAuthorization: function(types
|
|
3481
|
+
requestAuthorization: function(types, options) {
|
|
3482
|
+
return craft._invoke('requestHealthAuthorization', {types: types || [], readOnly: Boolean(options && options.write === false)});
|
|
3483
|
+
},
|
|
3391
3484
|
getData: function(type, options) {
|
|
3392
3485
|
options = options || {};
|
|
3393
3486
|
return craft._invoke('getHealthData', {type: type, startDate: options.startDate, endDate: options.endDate});
|
|
@@ -5125,7 +5218,7 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5125
5218
|
}
|
|
5126
5219
|
|
|
5127
5220
|
// MARK: - Health
|
|
5128
|
-
private func requestHealthAuthorization(types: [String], callbackId: String?) {
|
|
5221
|
+
private func requestHealthAuthorization(types: [String], readOnly: Bool = false, callbackId: String?) {
|
|
5129
5222
|
guard let healthStore = healthStore else {
|
|
5130
5223
|
rejectCallback(callbackId, error: "HealthKit not available")
|
|
5131
5224
|
return
|
|
@@ -5188,6 +5281,9 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5188
5281
|
}
|
|
5189
5282
|
}
|
|
5190
5283
|
|
|
5284
|
+
// A page that only reads asks only to read: the sheet then says
|
|
5285
|
+
// "access", not "access and update".
|
|
5286
|
+
if readOnly { shareTypes.removeAll() }
|
|
5191
5287
|
healthStore.requestAuthorization(toShare: shareTypes, read: readTypes) { [weak self] success, error in
|
|
5192
5288
|
if success {
|
|
5193
5289
|
self?.resolveCallback(callbackId, result: true)
|