craft-native 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/android/src/index.js +2 -1
- package/dist/api/mobile.d.ts +10 -1
- package/dist/cli.js +5 -3
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +2 -0
- package/dist/ios/src/index.js +2 -1
- package/dist/ios/templates/CraftApp.swift +106 -10
- package/dist/mobile.js +2 -2
- package/package.json +1 -1
|
@@ -361,9 +361,10 @@ async function init(options) {
|
|
|
361
361
|
`);
|
|
362
362
|
const finalPackageName = packageName || `com.craft.${generatedPackageSegment(name)}`;
|
|
363
363
|
const packagePath = finalPackageName.replace(/\./g, "/");
|
|
364
|
+
const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
|
|
364
365
|
const config = {
|
|
365
366
|
...DEFAULT_CONFIG,
|
|
366
|
-
...
|
|
367
|
+
...given,
|
|
367
368
|
appName: name,
|
|
368
369
|
packageName: finalPackageName
|
|
369
370
|
};
|
package/dist/api/mobile.d.ts
CHANGED
|
@@ -730,6 +730,10 @@ export declare const pushNotifications: {
|
|
|
730
730
|
export type HealthDataType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts' | 'restingHeartRate' | 'heartRateVariability' | 'bodyMass' | 'sleep';
|
|
731
731
|
/** The types with one value per day. `sleep` is hours asleep, counted on the day you woke. */
|
|
732
732
|
export type HealthDailyType = Exclude<HealthDataType, 'workouts'>;
|
|
733
|
+
export interface HealthAuthorizationOptions {
|
|
734
|
+
/** `false` asks to read only. Default: also allowed to save workouts. */
|
|
735
|
+
write?: boolean;
|
|
736
|
+
}
|
|
733
737
|
export interface HealthDataOptions {
|
|
734
738
|
startDate?: number;
|
|
735
739
|
endDate?: number;
|
|
@@ -791,7 +795,12 @@ export interface HealthDailyValue {
|
|
|
791
795
|
unit: string;
|
|
792
796
|
}
|
|
793
797
|
export declare const health: {
|
|
794
|
-
|
|
798
|
+
/**
|
|
799
|
+
* Ask for the health data a page needs. `{ write: false }` asks to read
|
|
800
|
+
* only, for an app that never writes back; by default it may also save
|
|
801
|
+
* workouts, energy and distance.
|
|
802
|
+
*/
|
|
803
|
+
requestAuthorization(types: HealthDataType[], options?: HealthAuthorizationOptions): Promise<boolean>;
|
|
795
804
|
getData(type: HealthDataType, options?: HealthDataOptions): Promise<HealthDataResult>;
|
|
796
805
|
saveWorkout(workout: HealthWorkout): Promise<HealthWorkoutResult>;
|
|
797
806
|
/** Workouts in Apple Health, newest first. iOS only for now. */
|
package/dist/cli.js
CHANGED
|
@@ -1404,12 +1404,13 @@ async function init(options) {
|
|
|
1404
1404
|
}
|
|
1405
1405
|
const finalBundleId = bundleId || `com.craft.${name.toLowerCase()}`;
|
|
1406
1406
|
const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
|
|
1407
|
+
const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
|
|
1407
1408
|
const config = {
|
|
1408
1409
|
...DEFAULT_CONFIG,
|
|
1409
1410
|
appName: displayName,
|
|
1410
1411
|
bundleId: finalBundleId,
|
|
1411
1412
|
teamId: teamId || "",
|
|
1412
|
-
...
|
|
1413
|
+
...given
|
|
1413
1414
|
};
|
|
1414
1415
|
if (config.enableBackgroundLocation)
|
|
1415
1416
|
config.enableGeolocation = true;
|
|
@@ -2004,9 +2005,10 @@ async function init2(options) {
|
|
|
2004
2005
|
`);
|
|
2005
2006
|
const finalPackageName = packageName || `com.craft.${generatedPackageSegment(name)}`;
|
|
2006
2007
|
const packagePath = finalPackageName.replace(/\./g, "/");
|
|
2008
|
+
const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
|
|
2007
2009
|
const config = {
|
|
2008
2010
|
...DEFAULT_CONFIG2,
|
|
2009
|
-
...
|
|
2011
|
+
...given,
|
|
2010
2012
|
appName: name,
|
|
2011
2013
|
packageName: finalPackageName
|
|
2012
2014
|
};
|
|
@@ -4619,7 +4621,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
4619
4621
|
`);
|
|
4620
4622
|
}
|
|
4621
4623
|
// package.json
|
|
4622
|
-
var version = "0.0.
|
|
4624
|
+
var version = "0.0.101";
|
|
4623
4625
|
|
|
4624
4626
|
// bin/cli.ts
|
|
4625
4627
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/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/ios/src/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;
|
|
@@ -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)
|
package/dist/mobile.js
CHANGED
|
@@ -657,11 +657,11 @@ var pushNotifications = {
|
|
|
657
657
|
}
|
|
658
658
|
};
|
|
659
659
|
var health = {
|
|
660
|
-
async requestAuthorization(types) {
|
|
660
|
+
async requestAuthorization(types, options = {}) {
|
|
661
661
|
const craft = getCraftRoot();
|
|
662
662
|
if (!craft?.health?.requestAuthorization)
|
|
663
663
|
throw new Error("Health data is unavailable");
|
|
664
|
-
return Boolean(await craft.health.requestAuthorization(types));
|
|
664
|
+
return Boolean(await craft.health.requestAuthorization(types, options));
|
|
665
665
|
},
|
|
666
666
|
async getData(type, options = {}) {
|
|
667
667
|
const craft = getCraftRoot();
|