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.
@@ -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
- ...options.config,
367
+ ...given,
367
368
  appName: name,
368
369
  packageName: finalPackageName
369
370
  };
@@ -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
- requestAuthorization(types: HealthDataType[]): Promise<boolean>;
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
- ...options.config
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
- ...options.config,
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.99";
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
@@ -310,7 +310,7 @@ function craftBinaryNotFoundMessage(triedPath) {
310
310
  `);
311
311
  }
312
312
  // package.json
313
- var version = "0.0.99";
313
+ var version = "0.0.101";
314
314
 
315
315
  // src/sdk-version.ts
316
316
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ function craftBinaryNotFoundMessage(triedPath) {
58
58
  `);
59
59
  }
60
60
  // package.json
61
- var version = "0.0.99";
61
+ var version = "0.0.101";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -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;
@@ -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
- ...options.config
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
- let config = try? JSONDecoder().decode(CraftConfig.self, from: data) {
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) { return craft._invoke('requestHealthAuthorization', {types: 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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.99",
3
+ "version": "0.0.101",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",