craft-native 0.0.236 → 0.0.238

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 (34) 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 +7 -1
  4. package/dist/android/src/index.js +280 -98
  5. package/dist/android/templates/CraftBridge.kt.template +65 -54
  6. package/dist/android/templates/CraftNative.kt.template +27 -0
  7. package/dist/android/templates/CraftNativeCapabilities.kt.template +55 -6
  8. package/dist/android/templates/CraftNativeServices.kt.template +45 -35
  9. package/dist/android/templates/CraftNativeShell.kt.template +103 -20
  10. package/dist/android/templates/LocationRecordingService.kt.template +252 -46
  11. package/dist/android/templates/MainActivityNative.kt.template +163 -5
  12. package/dist/android-adb.d.ts +2 -0
  13. package/dist/api/index.d.ts +1 -1
  14. package/dist/api/mobile.d.ts +49 -14
  15. package/dist/browser/storage.d.ts +3 -0
  16. package/dist/browser.js +163 -51
  17. package/dist/cli.js +1 -1
  18. package/dist/components/index.d.ts +33 -0
  19. package/dist/index.cjs +126 -12
  20. package/dist/index.js +126 -12
  21. package/dist/ios/src/app-shortcuts.d.ts +11 -0
  22. package/dist/ios/src/index.d.ts +7 -0
  23. package/dist/ios/src/index.js +65 -1
  24. package/dist/ios/src/testing.d.ts +2 -0
  25. package/dist/ios/templates/CraftApp.swift +392 -88
  26. package/dist/ios/templates/CraftHeartRate.swift +141 -44
  27. package/dist/ios/templates/CraftNativeActions.swift +46 -48
  28. package/dist/ios/templates/CraftNativeScreen.swift +177 -10
  29. package/dist/ios/templates/CraftPersistentStorage.swift +99 -0
  30. package/dist/ios/templates/CraftRecordingArchive.swift +85 -0
  31. package/dist/ios/templates/testing/DriverTests.swift.template +8 -1
  32. package/dist/mobile-testing.js +30 -4
  33. package/dist/mobile.js +169 -17
  34. package/package.json +1 -1
@@ -600,19 +600,43 @@ private class CraftNativeSvgView(context: Context) : View(context) {
600
600
  /** The Android host for the small, host-neutral native trail map contract. */
601
601
  private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMapReadyCallback {
602
602
  private val mapView = MapView(context)
603
+ private val scaleView = LinearLayout(context).apply {
604
+ orientation = LinearLayout.VERTICAL
605
+ setPadding(4, 2, 4, 2)
606
+ background = GradientDrawable().apply { setColor(Color.argb(230, 255, 255, 255)); cornerRadius = 4f }
607
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
608
+ }
609
+ private val scaleLabel = TextView(context).apply { textSize = 11f; setTextColor(Color.rgb(37, 51, 33)) }
610
+ private val scaleLine = View(context).apply { setBackgroundColor(Color.rgb(37, 51, 33)) }
603
611
  private var googleMap: GoogleMap? = null
612
+ private var tileOverlay: com.google.android.gms.maps.model.TileOverlay? = null
613
+ private var tileTemplate: String? = null
614
+ private val tileAttribution = TextView(context).apply { textSize = 9f; setTextColor(Color.DKGRAY); setBackgroundColor(Color.argb(220, 255, 255, 255)); maxLines = 2; visibility = View.GONE }
604
615
  private var pendingProps = JSONObject()
605
616
  private var pendingChildren = JSONArray()
606
617
  private val markers = mutableMapOf<String, Marker>()
607
618
  private val polylines = mutableMapOf<String, Polyline>()
608
619
  private val markerHandlers = mutableMapOf<String, String>()
609
620
  private var onMarkerTap: ((String) -> Unit)? = null
621
+ var onMapPress: ((LatLng) -> Unit)? = null
622
+ var onRegionChangeComplete: ((JSONObject) -> Unit)? = null
623
+ private var userChangingCamera = false
624
+ private var markerBearings = mutableMapOf<String, Float>()
625
+ private var directionIcon: com.google.android.gms.maps.model.BitmapDescriptor? = null
610
626
  private var lifecycleStarted = false
611
627
  private var destroyed = false
612
628
  private var initialCameraKey: String? = null
613
629
 
614
630
  init {
615
631
  addView(mapView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
632
+ scaleView.addView(scaleLabel)
633
+ scaleView.addView(scaleLine, LinearLayout.LayoutParams(1, 2))
634
+ addView(scaleView, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM or Gravity.START).apply {
635
+ leftMargin = (8 * resources.displayMetrics.density).roundToInt()
636
+ bottomMargin = (24 * resources.displayMetrics.density).roundToInt()
637
+ })
638
+ scaleView.visibility = View.GONE
639
+ addView(tileAttribution, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.BOTTOM or Gravity.END).apply { rightMargin = (8 * resources.displayMetrics.density).roundToInt(); bottomMargin = (8 * resources.displayMetrics.density).roundToInt() })
616
640
  mapView.onCreate(null)
617
641
  mapView.getMapAsync(this)
618
642
  }
@@ -620,6 +644,24 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
620
644
  override fun onMapReady(map: GoogleMap) {
621
645
  if (destroyed) return
622
646
  googleMap = map
647
+ map.setOnMapClickListener { point -> onMapPress?.invoke(point) }
648
+ map.setOnCameraMoveStartedListener { reason ->
649
+ if (reason == GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE) userChangingCamera = true
650
+ }
651
+ map.setOnCameraMoveListener { updateScale() }
652
+ map.setOnCameraIdleListener {
653
+ updateScale()
654
+ if (userChangingCamera) {
655
+ userChangingCamera = false
656
+ val bounds = map.projection.visibleRegion.latLngBounds
657
+ val position = map.cameraPosition
658
+ val longitudeDelta = (bounds.northeast.longitude - bounds.southwest.longitude + 360) % 360
659
+ onRegionChangeComplete?.invoke(JSONObject()
660
+ .put("center", JSONObject().put("latitude", position.target.latitude).put("longitude", position.target.longitude))
661
+ .put("span", JSONObject().put("latitudeDelta", bounds.northeast.latitude - bounds.southwest.latitude).put("longitudeDelta", longitudeDelta))
662
+ .put("heading", position.bearing).put("pitch", position.tilt))
663
+ }
664
+ }
623
665
  map.setOnMarkerClickListener { marker ->
624
666
  val id = markers.entries.firstOrNull { it.value == marker }?.key ?: return@setOnMarkerClickListener false
625
667
  onMarkerTap?.invoke(id)
@@ -642,6 +684,10 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
642
684
 
643
685
  private fun applyPending() {
644
686
  val map = googleMap ?: return
687
+ applyMapPadding()
688
+ map.uiSettings.isCompassEnabled = craftNativeBoolean(pendingProps.opt("showsCompass"))
689
+ // The toolbar is replaced by authored actions; the scale uses the native projection.
690
+ map.uiSettings.isMapToolbarEnabled = false
645
691
  map.uiSettings.isScrollGesturesEnabled = craftNativeBoolean(pendingProps.opt("scrollEnabled"), true)
646
692
  map.uiSettings.isZoomGesturesEnabled = craftNativeBoolean(pendingProps.opt("zoomEnabled"), true)
647
693
  map.uiSettings.isRotateGesturesEnabled = craftNativeBoolean(pendingProps.opt("rotateEnabled"))
@@ -651,9 +697,11 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
651
697
  "terrain" -> GoogleMap.MAP_TYPE_TERRAIN
652
698
  else -> GoogleMap.MAP_TYPE_NORMAL
653
699
  }
700
+ applyTileOverlay()
654
701
  val showUserLocation = craftNativeBoolean(pendingProps.opt("showsUserLocation")) && hasLocationPermission()
655
702
  runCatching { map.isMyLocationEnabled = showUserLocation }
656
703
  val nextMarkers = mutableMapOf<String, Pair<LatLng, String?>>()
704
+ val nextBearings = mutableMapOf<String, Float>()
657
705
  val nextHandlers = mutableMapOf<String, String>()
658
706
  val nextPolylines = mutableMapOf<String, Triple<List<LatLng>, Int, Float>>()
659
707
  for (index in 0 until pendingChildren.length()) {
@@ -668,6 +716,7 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
668
716
  if (type == "mapmarker" || type == "marker") {
669
717
  points.firstOrNull()?.let { point ->
670
718
  nextMarkers[id] = point to (props.opt("title") as? String)
719
+ (props.opt("bearing") as? Number)?.toDouble()?.takeIf { it.isFinite() }?.let { nextBearings[id] = (it % 360).toFloat() }
671
720
  listOf(events.opt("onPress"), events.opt("onMarkerPress"))
672
721
  .firstNotNullOfOrNull(::craftNativeOptionalString)
673
722
  ?.let { nextHandlers[id] = it }
@@ -694,14 +743,19 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
694
743
  }
695
744
  }
696
745
  }
746
+ markerBearings = nextBearings
697
747
  markers.keys.filter { nextMarkers[it] == null }.forEach { markers.remove(it)?.remove() }
698
748
  for ((id, value) in nextMarkers) {
699
749
  val marker = markers[id]
700
750
  if (marker != null) {
701
751
  marker.position = value.first
702
752
  marker.title = value.second
753
+ marker.setIcon(if (markerBearings.containsKey(id)) directionMarkerIcon() else BitmapDescriptorFactory.defaultMarker())
754
+ marker.isFlat = markerBearings.containsKey(id)
755
+ marker.rotation = markerBearings[id] ?: 0f
756
+ marker.setAnchor(0.5f, if (markerBearings.containsKey(id)) 0.5f else 1f)
703
757
  } else {
704
- map.addMarker(MarkerOptions().position(value.first).title(value.second).icon(BitmapDescriptorFactory.defaultMarker()))?.let { markers[id] = it }
758
+ map.addMarker(MarkerOptions().position(value.first).title(value.second).icon(if (markerBearings.containsKey(id)) directionMarkerIcon() else BitmapDescriptorFactory.defaultMarker()).flat(markerBearings.containsKey(id)).rotation(markerBearings[id] ?: 0f).anchor(0.5f, if (markerBearings.containsKey(id)) 0.5f else 1f))?.let { markers[id] = it }
705
759
  }
706
760
  }
707
761
  markerHandlers.clear()
@@ -734,17 +788,99 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
734
788
  initialCameraKey = key
735
789
  }
736
790
  } else if (craftNativeBoolean(pendingProps.opt("fitToBounds"))) {
737
- val key = "fit:${overlayKey(nextMarkers, nextPolylines)}"
791
+ val key = "fit:${overlayKey(nextMarkers, nextPolylines)}:${pendingProps.optJSONObject("edgePadding")}"
738
792
  if (initialCameraKey != key) {
739
793
  if (fitToBounds()) initialCameraKey = key
740
794
  }
741
795
  } else {
742
796
  initialCameraKey = null
743
797
  }
798
+ applyCameraOrientation()
799
+ updateScale()
800
+ }
801
+
802
+ private fun applyTileOverlay() {
803
+ val map = googleMap ?: return
804
+ val options = pendingProps.optJSONObject("tileOverlay")
805
+ val template = options?.optString("urlTemplate").orEmpty()
806
+ val credit = options?.optString("attribution").orEmpty()
807
+ val next = template.takeIf { it.startsWith("https://") && listOf("{z}", "{x}", "{y}").all { key -> it.contains(key) } && credit.isNotBlank() }
808
+ if (next != tileTemplate) {
809
+ tileOverlay?.remove(); tileOverlay = null; tileTemplate = next
810
+ if (next != null) {
811
+ val provider = object : com.google.android.gms.maps.model.UrlTileProvider(256, 256) {
812
+ override fun getTileUrl(x: Int, y: Int, zoom: Int): java.net.URL? = runCatching { java.net.URL(next.replace("{x}", x.toString()).replace("{y}", y.toString()).replace("{z}", zoom.toString())) }.getOrNull()
813
+ }
814
+ tileOverlay = map.addTileOverlay(com.google.android.gms.maps.model.TileOverlayOptions().tileProvider(provider).zIndex(-1f))
815
+ }
816
+ }
817
+ if (next != null) map.mapType = GoogleMap.MAP_TYPE_NONE
818
+ tileAttribution.text = credit
819
+ tileAttribution.visibility = if (next == null) View.GONE else View.VISIBLE
820
+ }
821
+
822
+ private fun applyMapPadding() {
823
+ val authored = pendingProps.optJSONObject("edgePadding")
824
+ fun inset(name: String): Double = (authored?.opt(name) as? Number)?.toDouble()?.takeIf { it.isFinite() }?.coerceIn(0.0, 10_000.0)?.times(resources.displayMetrics.density) ?: 0.0
825
+ var left = inset("left"); var top = inset("top"); var right = inset("right"); var bottom = inset("bottom")
826
+ val horizontal = left + right
827
+ val vertical = top + bottom
828
+ val availableWidth = (width - 40 * resources.displayMetrics.density).coerceAtLeast(0f).toDouble()
829
+ val availableHeight = (height - 40 * resources.displayMetrics.density).coerceAtLeast(0f).toDouble()
830
+ if (horizontal > availableWidth && horizontal > 0) { left *= availableWidth / horizontal; right *= availableWidth / horizontal }
831
+ if (vertical > availableHeight && vertical > 0) { top *= availableHeight / vertical; bottom *= availableHeight / vertical }
832
+ googleMap?.setPadding(left.roundToInt(), top.roundToInt(), right.roundToInt(), bottom.roundToInt())
833
+ }
834
+
835
+ private fun updateScale() {
836
+ val map = googleMap ?: return
837
+ if (!craftNativeBoolean(pendingProps.opt("showsScale")) || width <= 0 || height <= 0) { scaleView.visibility = View.GONE; return }
838
+ val maximumWidth = (80 * resources.displayMetrics.density).roundToInt()
839
+ val origin = map.projection.fromScreenLocation(android.graphics.Point(width / 2, height / 2))
840
+ val end = map.projection.fromScreenLocation(android.graphics.Point(width / 2 + maximumWidth, height / 2))
841
+ val distance = FloatArray(1)
842
+ android.location.Location.distanceBetween(origin.latitude, origin.longitude, end.latitude, end.longitude, distance)
843
+ val metres = distance[0].toDouble()
844
+ if (!metres.isFinite() || metres <= 0) { scaleView.visibility = View.GONE; return }
845
+ val magnitude = Math.pow(10.0, kotlin.math.floor(kotlin.math.log10(metres)))
846
+ val length = listOf(5.0, 2.0, 1.0).firstOrNull { it * magnitude <= metres }?.times(magnitude) ?: magnitude
847
+ scaleLabel.text = if (length >= 1000) "${length / 1000} km" else "${length.roundToInt()} m"
848
+ scaleLine.layoutParams = LinearLayout.LayoutParams((maximumWidth * length / metres).roundToInt().coerceAtLeast(1), 2)
849
+ scaleView.visibility = View.VISIBLE
850
+ }
851
+
852
+ private fun applyCameraOrientation() {
853
+ val map = googleMap ?: return
854
+ val camera = map.cameraPosition
855
+ val pitch = (pendingProps.opt("cameraPitch") as? Number)?.toDouble()?.takeIf { it.isFinite() }?.coerceIn(0.0, 60.0)?.toFloat() ?: camera.tilt
856
+ val heading = (pendingProps.opt("cameraHeading") as? Number)?.toDouble()?.takeIf { it.isFinite() }?.toFloat() ?: camera.bearing
857
+ if (kotlin.math.abs(camera.tilt - pitch) > 0.01f || kotlin.math.abs(camera.bearing - heading) > 0.01f) {
858
+ map.moveCamera(CameraUpdateFactory.newCameraPosition(com.google.android.gms.maps.model.CameraPosition.Builder(camera).tilt(pitch).bearing(heading).build()))
859
+ }
860
+ }
861
+
862
+ private fun directionMarkerIcon(): com.google.android.gms.maps.model.BitmapDescriptor {
863
+ directionIcon?.let { return it }
864
+ val size = (26 * resources.displayMetrics.density).roundToInt().coerceAtLeast(24)
865
+ val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
866
+ val canvas = Canvas(bitmap)
867
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG)
868
+ paint.color = Color.WHITE
869
+ canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
870
+ paint.color = Color.rgb(36, 114, 75)
871
+ canvas.drawCircle(size / 2f, size / 2f, size * 0.43f, paint)
872
+ paint.color = Color.WHITE
873
+ paint.textSize = size * 0.76f
874
+ paint.textAlign = Paint.Align.CENTER
875
+ paint.typeface = Typeface.DEFAULT_BOLD
876
+ canvas.drawText("↑", size / 2f, size / 2f - (paint.ascent() + paint.descent()) / 2, paint)
877
+ return BitmapDescriptorFactory.fromBitmap(bitmap).also { directionIcon = it }
744
878
  }
745
879
 
746
880
  override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
747
881
  super.onSizeChanged(width, height, oldWidth, oldHeight)
882
+ applyMapPadding()
883
+ updateScale()
748
884
  // MapView can become ready before its host has a usable viewport. Do
749
885
  // not mark a fit as complete until the camera update can be issued.
750
886
  if (width > 0 && height > 0 && craftNativeBoolean(pendingProps.opt("fitToBounds")) && initialCameraKey == null) {
@@ -754,7 +890,10 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
754
890
 
755
891
  private fun moveCamera(bounds: LatLngBounds, padding: Int = 24) {
756
892
  post {
757
- if (width > 0 && height > 0) googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, padding))
893
+ if (width > 0 && height > 0) {
894
+ googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, padding))
895
+ applyCameraOrientation()
896
+ }
758
897
  }
759
898
  }
760
899
 
@@ -780,7 +919,12 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
780
919
  polylines.values.flatMap { it.points }.forEach { builder.include(it); count++ }
781
920
  if (count == 0) return false
782
921
  post {
783
- if (width > 0 && height > 0) map.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), 24))
922
+ if (width > 0 && height > 0) {
923
+ applyMapPadding()
924
+ val padding = if (pendingProps.has("edgePadding")) 0 else (24 * resources.displayMetrics.density).roundToInt()
925
+ map.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), padding))
926
+ applyCameraOrientation()
927
+ }
784
928
  }
785
929
  return true
786
930
  }
@@ -811,6 +955,10 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
811
955
  lifecycleStarted = false
812
956
  }
813
957
  onMarkerTap = null
958
+ onMapPress = null
959
+ onRegionChangeComplete = null
960
+ directionIcon = null
961
+ tileOverlay?.remove(); tileOverlay = null
814
962
  markerHandlers.clear()
815
963
  markers.values.forEach { it.remove() }
816
964
  markers.clear()
@@ -5746,6 +5894,16 @@ class MainActivity : AppCompatActivity() {
5746
5894
  }
5747
5895
  "Map" -> (previous as? CraftNativeMapView ?: CraftNativeMapView(this)).apply {
5748
5896
  val mapMarkerHandler = craftNativeOptionalString(events.opt("onMarkerPress"))
5897
+ onMapPress = { point ->
5898
+ craftNativeOptionalString(events.opt("onPress"))?.let { handler ->
5899
+ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject().put("latitude", point.latitude).put("longitude", point.longitude)))
5900
+ }
5901
+ }
5902
+ onRegionChangeComplete = { region ->
5903
+ craftNativeOptionalString(events.opt("onRegionChangeComplete"))?.let { handler ->
5904
+ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", region))
5905
+ }
5906
+ }
5749
5907
  configure(props, children) { markerId ->
5750
5908
  (markerHandler(markerId) ?: mapMarkerHandler)?.let { handler ->
5751
5909
  send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject().put("id", markerId)))
@@ -7153,7 +7311,7 @@ class MainActivity : AppCompatActivity() {
7153
7311
 
7154
7312
  override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
7155
7313
  super.onRequestPermissionsResult(requestCode, permissions, grantResults)
7156
- if (::capabilities.isInitialized) capabilities.onBluetoothPermissions(requestCode)
7314
+ if (::capabilities.isInitialized) { capabilities.onBluetoothPermissions(requestCode); capabilities.onNotificationPermissions(requestCode) }
7157
7315
  if (::services.isInitialized) services.onRequestPermissionsResult(requestCode)
7158
7316
  }
7159
7317
 
@@ -0,0 +1,2 @@
1
+ /** Restart adbd once and require a rooted, explicitly selected disposable emulator. */
2
+ export declare function requireAndroidEmulatorRoot(adb: string, serial: string, timeoutMs?: number): Promise<void>;
@@ -17,7 +17,7 @@ export type { HttpClientOptions, RequestOptions, HttpResponse, WebSocketOptions
17
17
  export { crypto, uuid, randomString, hmac, timingSafeEqual, hashPassword, verifyPassword } from './crypto.js';
18
18
  export { env, getPlatform, isDesktop, isMobile, isCraft, getSystemInfo, exec, spawn, ChildProcess, cwd, homeDir, tempDir, exit, argv, open } from './process.js';
19
19
  export type { Platform, SystemInfo, ExecOptions, ExecResult, SpawnOptions } from './process.js';
20
- export { device, haptics, permissions, camera, biometrics, secureStorage, location, share, lifecycle, notifications, notifications as notification } from './mobile.js';
20
+ export { device, haptics, permissions, camera, biometrics, secureStorage, storage, location, share, lifecycle, notifications, notifications as notification } from './mobile.js';
21
21
  export type { DeviceInfo, DeviceCapabilities, HapticStyle, HapticNotificationType, PermissionType, PermissionStatus, CameraOptions as MobileCameraOptions, PhotoResult, BiometricType, Location, LocationOptions, ShareOptions, AppState, NotificationOptions as MobileNotificationOptions } from './mobile.js';
22
22
  export { carplay, appClips, liveActivities, sharePlay, storeKit, appIntents, tipKit, focusFilters } from './ios-advanced.js';
23
23
  export type { CarPlayTemplateType, CarPlayListItem, CarPlayGridItem, CarPlayTemplate, AppClipInvocation, LiveActivityContentState, LiveActivityAttributes, LiveActivityConfig, LiveActivityHandle, SharePlaySessionState, SharePlayParticipant, SharePlayActivity, ProductType, Product, Transaction, IntentParameterType, IntentParameter, AppIntent, TipDisplayFrequency, Tip, FocusStatus, FocusFilter } from './ios-advanced.js';
@@ -459,6 +459,8 @@ export declare const secureStorage: {
459
459
  * Location coordinates.
460
460
  */
461
461
  export interface Location {
462
+ /** Owner captured when a native recording emits this sample. */
463
+ recordingId?: string;
462
464
  latitude: number;
463
465
  longitude: number;
464
466
  altitude?: number;
@@ -478,6 +480,30 @@ export interface LocationOptions {
478
480
  /** Maximum age of cached position in milliseconds */
479
481
  maximumAge?: number;
480
482
  }
483
+ /** Persist this ID with the owning intent before starting a native recording. */
484
+ export interface RecordingStartOptions {
485
+ recordingId?: string;
486
+ }
487
+ /** Fence a recording operation to the ID returned by start or explicit recovery. */
488
+ export interface RecordingOwnershipOptions {
489
+ expectedId: string;
490
+ }
491
+ /** Secure IDs supplied by the platform, including WebView-free mobile hosts. */
492
+ export declare const crypto: {
493
+ randomUUID(): Promise<string>;
494
+ };
495
+ /** Native storage, including an existing-key atomic checkpoint. */
496
+ export declare const storage: {
497
+ supportsSetIfAbsent(): boolean;
498
+ supportsCompareAndSet(): boolean;
499
+ get<T = unknown>(key: string): Promise<T | null>;
500
+ set(key: string, value: unknown): Promise<boolean>;
501
+ remove(key: string): Promise<boolean>;
502
+ clear(): Promise<boolean>;
503
+ keys(): Promise<string[]>;
504
+ setIfAbsent(key: string, value: unknown): Promise<boolean>;
505
+ compareAndSet(key: string, expectedValue: unknown, nextValue: unknown): Promise<boolean>;
506
+ };
481
507
  export interface LocationRecordingState {
482
508
  id: string | null;
483
509
  active: boolean;
@@ -507,6 +533,8 @@ export interface LocationRecordingResult extends LocationRecordingState {
507
533
  * ```
508
534
  */
509
535
  export declare const location: {
536
+ supportsRecordingOwnership: () => boolean;
537
+ supportsRecordingIntentIdentity: () => boolean;
510
538
  /**
511
539
  * Get current position.
512
540
  *
@@ -528,12 +556,13 @@ export declare const location: {
528
556
  * @param watchId - Watch ID returned from watchPosition
529
557
  */
530
558
  clearWatch(watchId: number): void;
531
- startRecording(options?: LocationOptions): Promise<LocationRecordingState>;
532
- pauseRecording(): Promise<LocationRecordingState>;
533
- resumeRecording(): Promise<LocationRecordingState>;
534
- stopRecording(): Promise<LocationRecordingResult>;
535
- getRecordingState(): Promise<LocationRecordingState>;
536
- readRecording(): Promise<Location[]>;
559
+ startRecording(options?: LocationOptions & RecordingStartOptions): Promise<LocationRecordingState>;
560
+ pauseRecording(options?: RecordingOwnershipOptions): Promise<LocationRecordingState>;
561
+ resumeRecording(options?: RecordingOwnershipOptions): Promise<LocationRecordingState>;
562
+ stopRecording(options?: RecordingOwnershipOptions): Promise<LocationRecordingResult>;
563
+ getRecordingState(options?: RecordingOwnershipOptions): Promise<LocationRecordingState>;
564
+ acknowledgeRecording(options: RecordingOwnershipOptions): Promise<boolean>;
565
+ readRecording(options?: RecordingOwnershipOptions): Promise<Location[]>;
537
566
  };
538
567
  /**
539
568
  * Share options.
@@ -914,7 +943,7 @@ export interface HeartRateSample {
914
943
  /** Milliseconds between beats, when the strap sent them. */
915
944
  rr?: number[];
916
945
  }
917
- export type HeartRateMethod = 'getState' | 'requestAuthorization' | 'scan' | 'stopScan' | 'getDevices' | 'connect' | 'reconnect' | 'disconnect' | 'startRecording' | 'pauseRecording' | 'resumeRecording' | 'stopRecording' | 'getRecordingState' | 'readRecording';
946
+ export type HeartRateMethod = 'getState' | 'requestAuthorization' | 'scan' | 'stopScan' | 'getDevices' | 'connect' | 'reconnect' | 'disconnect' | 'startRecording' | 'pauseRecording' | 'resumeRecording' | 'stopRecording' | 'getRecordingState' | 'readRecording' | 'acknowledgeRecording';
918
947
  export declare function normalizeHeartRateDevice(value: unknown): HeartRateDevice | null;
919
948
  /** A state from native, made safe to render whatever an older or newer build sent. */
920
949
  export declare function normalizeHeartRateState(value: unknown): HeartRateState;
@@ -923,6 +952,8 @@ export declare function normalizeHeartRateReading(value: unknown): HeartRateRead
923
952
  /** A recording's samples, oldest first, one per timestamp, implausible ones left out. */
924
953
  export declare function normalizeHeartRateSamples(value: unknown): HeartRateSample[];
925
954
  export declare const heartRate: {
955
+ supportsRecordingOwnership: () => boolean;
956
+ supportsRecordingIntentIdentity: () => boolean;
926
957
  /** Whether this app can read a heart-rate strap. */
927
958
  isAvailable(): boolean;
928
959
  /** Whether this app build answers `method`; an older build may lack one. */
@@ -955,18 +986,20 @@ export declare const heartRate: {
955
986
  forget?: boolean;
956
987
  }) => Promise<HeartRateState>;
957
988
  /** Keep every reading natively from now on: screen locked, page suspended or app closed. */
958
- startRecording: () => Promise<HeartRateRecordingState>;
959
- pauseRecording: () => Promise<HeartRateRecordingState>;
960
- resumeRecording: () => Promise<HeartRateRecordingState>;
989
+ startRecording(options?: RecordingStartOptions): Promise<HeartRateRecordingState>;
990
+ pauseRecording(options?: RecordingOwnershipOptions): Promise<HeartRateRecordingState>;
991
+ resumeRecording(options?: RecordingOwnershipOptions): Promise<HeartRateRecordingState>;
961
992
  /** Ends the recording and hands back every reading it kept. */
962
- stopRecording: () => Promise<HeartRateRecordingState & {
993
+ stopRecording(options?: RecordingOwnershipOptions): Promise<HeartRateRecordingState & {
963
994
  samples: HeartRateSample[];
964
995
  }>;
965
- getRecordingState: () => Promise<HeartRateRecordingState>;
996
+ getRecordingState(options?: RecordingOwnershipOptions): Promise<HeartRateRecordingState>;
997
+ acknowledgeRecording(options: RecordingOwnershipOptions): Promise<boolean>;
966
998
  /** The recording so far; only what came after `since` (ms) when given. */
967
- readRecording: (options?: {
999
+ readRecording(options?: {
968
1000
  since?: number;
969
- }) => Promise<HeartRateSample[]>;
1001
+ expectedId?: string;
1002
+ }): Promise<HeartRateSample[]>;
970
1003
  /** Every reading from the connected strap, about once a second. */
971
1004
  onReading: (callback: (reading: HeartRateReading) => void) => (() => void);
972
1005
  /** Each strap a scan finds. */
@@ -1685,6 +1718,8 @@ export declare const watchConnectivity: {
1685
1718
  onReachabilityChange(callback: (reachable: boolean) => void): () => void;
1686
1719
  };
1687
1720
  declare const mobile: {
1721
+ crypto: typeof crypto;
1722
+ storage: typeof storage;
1688
1723
  device: typeof device;
1689
1724
  haptics: typeof haptics;
1690
1725
  permissions: typeof permissions;
@@ -6,8 +6,11 @@ export interface BrowserStorageOptions {
6
6
  accountKeys?: string[];
7
7
  hash: (value: string) => Promise<string>;
8
8
  changed: (name: string) => void;
9
+ /** A host lock primitive; browsers use their native Web Locks implementation. */
10
+ lock?: (<T>(name: string, operation: () => T | Promise<T>) => Promise<T>) | null;
9
11
  }
10
12
  export interface BrowserStorage {
13
+ atomicStorage: boolean;
11
14
  craft: {
12
15
  storage: {
13
16
  getSync: (key: string) => unknown;