craft-native 0.0.237 → 0.0.241

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.
Files changed (40) hide show
  1. package/README.md +71 -0
  2. package/dist/android/src/emulator.d.ts +23 -0
  3. package/dist/android/src/index.d.ts +24 -2
  4. package/dist/android/src/index.js +362 -103
  5. package/dist/android/src/native-map.d.ts +7 -0
  6. package/dist/android/templates/CraftBridge.kt.template +67 -55
  7. package/dist/android/templates/CraftNative.kt.template +27 -0
  8. package/dist/android/templates/CraftNativeCapabilities.kt.template +36 -5
  9. package/dist/android/templates/CraftNativeMapGoogle.kt.template +440 -0
  10. package/dist/android/templates/CraftNativeMapLibre.kt.template +438 -0
  11. package/dist/android/templates/CraftNativeMessagingService.kt.template +3 -1
  12. package/dist/android/templates/CraftNativeNotificationReceiver.kt.template +2 -1
  13. package/dist/android/templates/CraftNativeServices.kt.template +45 -35
  14. package/dist/android/templates/CraftNativeShell.kt.template +103 -20
  15. package/dist/android/templates/LocationRecordingService.kt.template +255 -47
  16. package/dist/android/templates/MainActivityNative.kt.template +7 -428
  17. package/dist/android/templates/build.gradle.kts.app.template +1 -1
  18. package/dist/android/templates/build.gradle.kts.project.template +1 -1
  19. package/dist/android-adb.d.ts +2 -0
  20. package/dist/api/index.d.ts +1 -1
  21. package/dist/api/mobile.d.ts +49 -14
  22. package/dist/browser/storage.d.ts +3 -0
  23. package/dist/browser.js +163 -51
  24. package/dist/cli.js +1 -1
  25. package/dist/index.cjs +126 -12
  26. package/dist/index.js +126 -12
  27. package/dist/ios/src/index.d.ts +5 -0
  28. package/dist/ios/src/index.js +17 -1
  29. package/dist/ios/src/testing.d.ts +16 -2
  30. package/dist/ios/templates/CraftApp.swift +418 -93
  31. package/dist/ios/templates/CraftHeartRate.swift +141 -44
  32. package/dist/ios/templates/CraftNativeActions.swift +46 -48
  33. package/dist/ios/templates/CraftNativeScreen.swift +82 -17
  34. package/dist/ios/templates/CraftPersistentStorage.swift +99 -0
  35. package/dist/ios/templates/CraftRecordingArchive.swift +85 -0
  36. package/dist/ios/templates/testing/DriverTests.swift.template +45 -1
  37. package/dist/macos/templates/native_layout.zig +41 -5
  38. package/dist/mobile-testing.js +30 -4
  39. package/dist/mobile.js +169 -17
  40. package/package.json +1 -1
@@ -0,0 +1,7 @@
1
+ /** Build-time native map selection. Both providers are native Android views. */
2
+ export type AndroidMapProvider = 'google' | 'maplibre';
3
+ export declare function androidMapHost(provider?: AndroidMapProvider): {
4
+ template: string;
5
+ imports: string;
6
+ dependency: string;
7
+ };
@@ -410,6 +410,9 @@ class CraftBridge(
410
410
  pushNotifications: {{ENABLE_PUSH}},
411
411
  secureStorage: {{ENABLE_SECURE_STORAGE}},
412
412
  geolocation: {{ENABLE_GEOLOCATION}},
413
+ crypto: true,
414
+ locationRecordingOwnership: {{ENABLE_GEOLOCATION}} && ${CraftNative.supportsRecordingOwnership()},
415
+ locationRecordingIntentIdentity: {{ENABLE_GEOLOCATION}} && ${CraftNative.supportsRecordingIntentIdentity()},
413
416
  backgroundLocation: {{ENABLE_BACKGROUND_LOCATION}},
414
417
  clipboard: false,
415
418
  deviceInfo: true,
@@ -1769,16 +1772,41 @@ class CraftBridge(
1769
1772
  return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.crashReporterContext()); });
1770
1773
  }
1771
1774
  };
1775
+ function recordingCall(method, options) {
1776
+ return Promise.resolve().then(function() {
1777
+ if (options && Object.prototype.hasOwnProperty.call(options, 'expectedId') &&
1778
+ (typeof options.expectedId !== 'string' || !options.expectedId.trim())) {
1779
+ var invalid = new Error('expectedId must be a nonempty recording ID');
1780
+ invalid.code = 'INVALID_ARGUMENT';
1781
+ throw invalid;
1782
+ }
1783
+ if (options && Object.prototype.hasOwnProperty.call(options, 'recordingId') &&
1784
+ (typeof options.recordingId !== 'string' || !options.recordingId.trim())) {
1785
+ var invalidStart = new Error('recordingId must be a nonempty recording ID');
1786
+ invalidStart.code = 'INVALID_ARGUMENT';
1787
+ throw invalidStart;
1788
+ }
1789
+ var result = JSON.parse(CraftAndroid.recordingAction(method, JSON.stringify(options || {})));
1790
+ if (!result.ok) {
1791
+ var error = new Error(result.message || 'Recording failed');
1792
+ error.code = result.code;
1793
+ throw error;
1794
+ }
1795
+ return result.data;
1796
+ });
1797
+ }
1798
+ craft.crypto = { randomUUID: function() { return Promise.resolve().then(function() { return CraftAndroid.randomUUID(); }); } };
1772
1799
  craft.location = {
1773
1800
  getCurrentPosition: craft.getCurrentPosition,
1774
1801
  watchPosition: craft.watchPosition,
1775
1802
  clearWatch: craft.clearWatch,
1776
- startRecording: function(options) { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.startLocationRecording(JSON.stringify(options || {}))); }); },
1777
- pauseRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.pauseLocationRecording()); }); },
1778
- resumeRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.resumeLocationRecording()); }); },
1779
- stopRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.stopLocationRecording()); }); },
1780
- getRecordingState: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.getLocationRecordingState()); }); },
1781
- readRecording: function() { return Promise.resolve().then(function() { return JSON.parse(CraftAndroid.readLocationRecording()); }); }
1803
+ startRecording: function(options) { return recordingCall('startRecording', options); },
1804
+ pauseRecording: function(options) { return recordingCall('pauseRecording', options); },
1805
+ resumeRecording: function(options) { return recordingCall('resumeRecording', options); },
1806
+ stopRecording: function(options) { return recordingCall('stopRecording', options); },
1807
+ getRecordingState: function(options) { return recordingCall('getRecordingState', options); },
1808
+ readRecording: function(options) { return recordingCall('readRecording', options); },
1809
+ acknowledgeRecording: function(options) { return recordingCall('acknowledgeRecording', options); }
1782
1810
  };
1783
1811
  var shareApi = function(text, title) { return legacyShare(text, title); };
1784
1812
  shareApi.share = function(options) {
@@ -1840,6 +1868,9 @@ class CraftBridge(
1840
1868
  navigationGeneration += 1
1841
1869
  }
1842
1870
 
1871
+ @JavascriptInterface
1872
+ fun randomUUID(): String = CraftNative.randomUUID() ?: java.util.UUID.randomUUID().toString()
1873
+
1843
1874
  // ==================== Haptics ====================
1844
1875
 
1845
1876
  /**
@@ -2738,75 +2769,55 @@ class CraftBridge(
2738
2769
  }
2739
2770
 
2740
2771
  @JavascriptInterface
2741
- fun startLocationRecording(optionsJson: String): String {
2742
- // Null is "Zig declined". A permission-denied answer is not null — it
2743
- // is a state object saying so, and returning it here is the answer.
2744
- CraftNative.startLocationRecording(activity)?.let { return it }
2772
+ fun recordingAction(method: String, optionsJson: String): String {
2773
+ if (!{{ENABLE_GEOLOCATION}}) return JSONObject().put("ok", false).put("code", "CAPABILITY_DISABLED").put("message", "Geolocation is disabled").toString()
2774
+ val result = CraftNative.recordingAction(activity, method, optionsJson)
2775
+ ?: CraftLocationRecordingStore.perform(activity, method, optionsJson)
2776
+ if (method == "startRecording" && JSONObject(result).optBoolean("ok")) {
2777
+ CraftLocationRecordingStore.listen(activity, JSONObject(result).getJSONObject("data").getString("id")) { location, id ->
2778
+ val detail = mapOf<String, Any>("latitude" to location.latitude, "longitude" to location.longitude,
2779
+ "accuracy" to location.accuracy, "altitude" to location.altitude, "speed" to location.speed,
2780
+ "heading" to location.bearing, "timestamp" to location.time, "recordingId" to id)
2781
+ sendEvent("craftLocationUpdate", detail)
2782
+ }
2783
+ }
2784
+ return result
2785
+ }
2745
2786
 
2746
- if (!hasForegroundLocationPermission()) {
2747
- return JSONObject().apply {
2748
- put("id", JSONObject.NULL)
2749
- put("active", false)
2750
- put("paused", false)
2751
- put("startedAt", JSONObject.NULL)
2752
- put("sampleCount", 0)
2753
- }.toString()
2754
- }
2755
- val id = UUID.randomUUID().toString()
2756
- CraftLocationRecordingStore.start(activity, id, System.currentTimeMillis())
2757
- ContextCompat.startForegroundService(
2758
- activity,
2759
- Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_START)
2760
- )
2761
- return CraftLocationRecordingStore.state(activity).toString()
2787
+ private fun legacyRecordingAction(method: String, optionsJson: String = "{}"): String {
2788
+ val answer = JSONObject(recordingAction(method, optionsJson))
2789
+ if (!answer.optBoolean("ok")) throw com.craft.runtime.CraftRecordingException(answer.optString("code"), answer.optString("message"))
2790
+ return answer.get("data").toString()
2762
2791
  }
2763
2792
 
2764
2793
  @JavascriptInterface
2765
- fun pauseLocationRecording(): String {
2766
- CraftNative.pauseLocationRecording(activity)?.let { return it }
2794
+ fun startLocationRecording(optionsJson: String): String {
2795
+ return legacyRecordingAction("startRecording", optionsJson)
2796
+ }
2767
2797
 
2768
- CraftLocationRecordingStore.setPaused(activity, true)
2769
- return CraftLocationRecordingStore.state(activity).toString()
2798
+ @JavascriptInterface
2799
+ fun pauseLocationRecording(): String {
2800
+ return legacyRecordingAction("pauseRecording")
2770
2801
  }
2771
2802
 
2772
2803
  @JavascriptInterface
2773
2804
  fun resumeLocationRecording(): String {
2774
- CraftNative.resumeLocationRecording(activity)?.let { return it }
2775
-
2776
- CraftLocationRecordingStore.setPaused(activity, false)
2777
- if (CraftLocationRecordingStore.isActive(activity)) {
2778
- ContextCompat.startForegroundService(
2779
- activity,
2780
- Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_START)
2781
- )
2782
- }
2783
- return CraftLocationRecordingStore.state(activity).toString()
2805
+ return legacyRecordingAction("resumeRecording")
2784
2806
  }
2785
2807
 
2786
2808
  @JavascriptInterface
2787
2809
  fun stopLocationRecording(): String {
2788
- CraftNative.stopLocationRecording(activity)?.let { return it }
2789
-
2790
- CraftLocationRecordingStore.stop(activity)
2791
- val result = CraftLocationRecordingStore.state(activity, includeLocations = true).toString()
2792
- activity.stopService(Intent(activity, LocationRecordingService::class.java).setAction(LocationRecordingService.ACTION_STOP))
2793
- return result
2810
+ return legacyRecordingAction("stopRecording")
2794
2811
  }
2795
2812
 
2796
2813
  @JavascriptInterface
2797
2814
  fun getLocationRecordingState(): String {
2798
- // Null is "Zig declined", not "no recording" — an absent recording is
2799
- // a state object saying so, and returning it here is the answer.
2800
- CraftNative.getLocationRecordingState(activity)?.let { return it }
2801
-
2802
- return CraftLocationRecordingStore.state(activity).toString()
2815
+ return legacyRecordingAction("getRecordingState")
2803
2816
  }
2804
2817
 
2805
2818
  @JavascriptInterface
2806
2819
  fun readLocationRecording(): String {
2807
- CraftNative.readLocationRecording(activity)?.let { return it }
2808
-
2809
- return CraftLocationRecordingStore.locations(activity).toString()
2820
+ return legacyRecordingAction("readRecording")
2810
2821
  }
2811
2822
 
2812
2823
  // ==================== Clipboard ====================
@@ -3464,7 +3475,8 @@ class CraftBridge(
3464
3475
  val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
3465
3476
 
3466
3477
  val builder = androidx.core.app.NotificationCompat.Builder(activity, notificationChannelId)
3467
- .setSmallIcon(android.R.drawable.ic_dialog_info)
3478
+ .setSmallIcon(R.drawable.craft_notification_icon)
3479
+ .setColor(ContextCompat.getColor(activity, R.color.craft_notification_color))
3468
3480
  .setContentTitle(title)
3469
3481
  .setContentText(body)
3470
3482
  .setPriority(androidx.core.app.NotificationCompat.PRIORITY_DEFAULT)
@@ -230,6 +230,7 @@ object CraftNative {
230
230
  }
231
231
 
232
232
  private external fun nativeGetDeviceInfo(activity: Activity): String?
233
+ private external fun nativeRandomUUID(): String?
233
234
  private external fun nativeGetMemoryUsage(): String?
234
235
  private external fun nativeLog(message: String): Boolean
235
236
  private external fun nativeFlexRow(
@@ -437,6 +438,8 @@ object CraftNative {
437
438
  private external fun nativeSetKeepAwake(activity: Activity, enabled: Boolean): Boolean
438
439
  private external fun nativeDownloadFile(activity: Activity, url: String, filename: String): Boolean
439
440
  private external fun nativeSaveFile(activity: Activity, data: String, filename: String): Boolean
441
+ private external fun nativeRecordingAction(activity: Activity, method: String, options: String): String?
442
+ private external fun nativeRecordingOwnershipVersion(): Int
440
443
  private external fun nativeGetLocationRecordingState(activity: Activity): String?
441
444
  private external fun nativeReadLocationRecording(activity: Activity): String?
442
445
  private external fun nativeStartLocationRecording(activity: Activity): String?
@@ -487,6 +490,11 @@ object CraftNative {
487
490
  }
488
491
  }
489
492
 
493
+ fun randomUUID(): String? {
494
+ if (!isAvailable) return null
495
+ return try { nativeRandomUUID() } catch (_: UnsatisfiedLinkError) { null }
496
+ }
497
+
490
498
  /** The JVM heap as JSON, or null to use the Kotlin implementation. */
491
499
  fun getMemoryUsage(): String? {
492
500
  if (!isAvailable) return null
@@ -1921,6 +1929,25 @@ object CraftNative {
1921
1929
  * The read side of the recording store. Null is "Zig declined", not "no
1922
1930
  * recording" — an absent recording is a state object saying so.
1923
1931
  */
1932
+ fun supportsRecordingOwnership(): Boolean {
1933
+ if (!isAvailable) return true
1934
+ return try { nativeRecordingOwnershipVersion() >= 1 } catch (_: UnsatisfiedLinkError) { false }
1935
+ }
1936
+
1937
+ fun supportsRecordingIntentIdentity(): Boolean {
1938
+ if (!isAvailable) return true
1939
+ return try { nativeRecordingOwnershipVersion() >= 2 } catch (_: UnsatisfiedLinkError) { false }
1940
+ }
1941
+
1942
+ fun recordingAction(activity: Activity, method: String, options: String): String? {
1943
+ if (!isAvailable) return null
1944
+ return try { nativeRecordingAction(activity, method, options)
1945
+ ?: org.json.JSONObject().put("ok", false).put("code", "RECORDING_OWNERSHIP_UNSUPPORTED").put("message", "The linked runtime did not serve the recorder request").toString() }
1946
+ catch (error: UnsatisfiedLinkError) {
1947
+ org.json.JSONObject().put("ok", false).put("code", "RECORDING_OWNERSHIP_UNSUPPORTED").put("message", "The linked runtime lacks the shared recorder API").toString()
1948
+ }
1949
+ }
1950
+
1924
1951
  fun getLocationRecordingState(activity: Activity): String? {
1925
1952
  if (!isAvailable) return null
1926
1953
  return try {
@@ -116,6 +116,9 @@ internal class CraftNativeCapabilities(
116
116
 
117
117
  fun names(): JSONArray = JSONArray().apply {
118
118
  listOf("device", "clipboard", "haptics", "storage", "lifecycle", "fetch", "files").forEach(::put)
119
+ put("storageCompareAndSet")
120
+ put("storageSetIfAbsent")
121
+ put("crypto")
119
122
  if (config.optBoolean("enableBluetooth")) put("bluetooth")
120
123
  if (config.optBoolean("enableBiometric")) put("biometric")
121
124
  if (config.optBoolean("enableSecureStorage")) put("secureStorage")
@@ -138,11 +141,19 @@ internal class CraftNativeCapabilities(
138
141
  return
139
142
  }
140
143
  when (module) {
144
+ "Crypto" -> {
145
+ if (method == "randomUUID") complete(requestToken, success(UUID.randomUUID().toString()), completion)
146
+ else complete(requestToken, failure("UNKNOWN_ACTION", "Unsupported Crypto method"), completion)
147
+ }
141
148
  "Bluetooth" -> {
142
149
  if (!config.optBoolean("enableBluetooth")) complete(requestToken, failure("CAPABILITY_DISABLED", "Bluetooth is disabled"), completion)
143
150
  else bluetooth.perform(method, args) { complete(requestToken, it, completion) }
144
151
  }
145
- "Storage" -> complete(requestToken, storage(method, args), completion)
152
+ "Storage" -> mainHandler.post {
153
+ val answer = runCatching { storage(method, args) }
154
+ .getOrElse { failure("STORAGE_ERROR", it.message ?: "Storage operation failed") }
155
+ complete(requestToken, answer, completion)
156
+ }
146
157
  "SecureStorage" -> {
147
158
  if (!config.optBoolean("enableSecureStorage")) complete(requestToken, failure("CAPABILITY_DISABLED", "Secure storage is disabled"), completion)
148
159
  else complete(requestToken, secureStorage(method, args), completion)
@@ -648,9 +659,9 @@ internal class CraftNativeCapabilities(
648
659
  }
649
660
  }
650
661
 
651
- private fun storage(method: String, args: JSONArray): CraftNativeCapabilityResult {
662
+ private fun storage(method: String, args: JSONArray): CraftNativeCapabilityResult = synchronized(CraftNativeStorage.lock) {
652
663
  if (method == "clear") {
653
- storage.edit().clear().commit()
664
+ if (!CraftNativeStorage.commit(storage) { it.clear() }) return failure("STORAGE_ERROR", "Storage could not be cleared")
654
665
  storeObserver?.invoke("storage", null, null)
655
666
  return success(true)
656
667
  }
@@ -666,13 +677,33 @@ internal class CraftNativeCapabilities(
666
677
  if (args.length() != 2) return failure("INVALID_ARGUMENT", "Storage.set needs a key and value")
667
678
  val value = jsonString(args.opt(1))
668
679
  ?: return failure("INVALID_ARGUMENT", "Storage values must be JSON serializable")
669
- if (storage.edit().putString(key, value).commit()) {
680
+ if (CraftNativeStorage.commit(storage) { it.putString(key, value) }) {
670
681
  storeObserver?.invoke("storage", key, value)
671
682
  success(true)
672
683
  }
673
684
  else failure("STORAGE_ERROR", "Storage could not persist the value")
674
685
  }
675
- "remove" -> success(storage.edit().remove(key).commit().also { storeObserver?.invoke("storage", key, null) })
686
+ "compareAndSet" -> {
687
+ if (args.length() != 3) return failure("INVALID_ARGUMENT", "Storage.compareAndSet needs a key, expected value and next value")
688
+ val current = storage.getString(key, null) ?: return success(false)
689
+ if (!CraftNativeStorage.jsonEqual(JSONTokener(current).nextValue(), args.opt(1))) return success(false)
690
+ val next = jsonString(args.opt(2)) ?: return failure("INVALID_ARGUMENT", "Storage values must be JSON serializable")
691
+ if (!CraftNativeStorage.commit(storage) { it.putString(key, next) }) return failure("STORAGE_ERROR", "Storage could not persist the checkpoint")
692
+ storeObserver?.invoke("storage", key, next)
693
+ success(true)
694
+ }
695
+ "setIfAbsent" -> {
696
+ if (args.length() != 2) return failure("INVALID_ARGUMENT", "Storage.setIfAbsent needs a key and value")
697
+ if (storage.contains(key)) return success(false)
698
+ val value = jsonString(args.opt(1)) ?: return failure("INVALID_ARGUMENT", "Storage values must be JSON serializable")
699
+ if (!CraftNativeStorage.commit(storage) { it.putString(key, value) }) return failure("STORAGE_ERROR", "Storage could not persist the new intent")
700
+ storeObserver?.invoke("storage", key, value)
701
+ success(true)
702
+ }
703
+ "remove" -> if (CraftNativeStorage.commit(storage) { it.remove(key) }) {
704
+ storeObserver?.invoke("storage", key, null)
705
+ success(true)
706
+ } else failure("STORAGE_ERROR", "Storage could not remove the value")
676
707
  else -> failure("UNKNOWN_ACTION", "Unsupported Storage method")
677
708
  }
678
709
  }