craft-native 0.0.115 → 0.0.119

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.
@@ -3,13 +3,17 @@ package {{PACKAGE_NAME}}
3
3
  import android.content.ClipData
4
4
  import android.content.ClipboardManager
5
5
  import android.content.Context
6
+ import android.content.Intent
6
7
  import android.graphics.BitmapFactory
7
8
  import android.graphics.Color
8
9
  import android.graphics.Paint
9
10
  import android.graphics.Typeface
10
11
  import android.graphics.drawable.GradientDrawable
12
+ import android.net.Uri
11
13
  import android.os.Build
12
14
  import android.os.Bundle
15
+ import android.os.Handler
16
+ import android.os.Looper
13
17
  import android.os.VibrationEffect
14
18
  import android.os.Vibrator
15
19
  import android.text.Editable
@@ -39,6 +43,7 @@ import androidx.javascriptengine.JavaScriptSandbox
39
43
  import androidx.javascriptengine.Message
40
44
  import androidx.javascriptengine.MessagePort
41
45
  import androidx.javascriptengine.MessagePortClient
46
+ import com.google.common.util.concurrent.ListenableFuture
42
47
  import org.json.JSONArray
43
48
  import org.json.JSONObject
44
49
  import java.net.URL
@@ -46,6 +51,8 @@ import java.util.concurrent.Executors
46
51
  import java.util.concurrent.Future
47
52
  import java.util.UUID
48
53
 
54
+ private const val CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS = 30_000L
55
+
49
56
  private class CraftNativeScrollView(context: Context) : FrameLayout(context) {
50
57
  val content = LinearLayout(context)
51
58
  private val vertical = ScrollView(context)
@@ -87,6 +94,9 @@ class MainActivity : AppCompatActivity() {
87
94
  val flatListOwners = mutableMapOf<String, String>()
88
95
  val flatListOwnerByView = mutableMapOf<CraftNativeFlatList, String>()
89
96
  val mutations = CraftNativeMutationDocument()
97
+ val capabilityScope = UUID.randomUUID().toString()
98
+ val pendingCapabilities = mutableSetOf<String>()
99
+ val capabilityTimeouts = mutableMapOf<String, Runnable>()
90
100
  var active = true
91
101
 
92
102
  fun close() {
@@ -100,7 +110,16 @@ class MainActivity : AppCompatActivity() {
100
110
  private var sandbox: JavaScriptSandbox? = null
101
111
  private val screens = mutableListOf<Screen>()
102
112
  private var bundle = ""
113
+ private lateinit var config: JSONObject
114
+ private lateinit var capabilities: CraftNativeCapabilities
115
+ private val capabilityHandler = Handler(Looper.getMainLooper())
103
116
  private var hapticsEnabled = false
117
+ private var clipboardEnabled = false
118
+ internal var nativeAppState = "inactive"
119
+ private set
120
+ private var deliveredInitialDeepLink = false
121
+ private var initialDeepLink: Uri? = null
122
+ private var initialDeepLinkClaimed = false
104
123
  private var defaultTextColor = Color.WHITE
105
124
  private var useMessagePorts = false
106
125
  private val imageExecutor = Executors.newFixedThreadPool(2)
@@ -110,14 +129,19 @@ class MainActivity : AppCompatActivity() {
110
129
 
111
130
  override fun onCreate(savedInstanceState: Bundle?) {
112
131
  super.onCreate(savedInstanceState)
132
+ initialDeepLink = savedInstanceState?.getString("craft.initialDeepLink")?.let(Uri::parse) ?: intent?.data
133
+ initialDeepLinkClaimed = savedInstanceState?.getBoolean("craft.initialDeepLinkClaimed", false) ?: false
134
+ deliveredInitialDeepLink = savedInstanceState?.getBoolean("craft.deliveredInitialDeepLink", false) ?: false
113
135
  setContentView(R.layout.activity_main)
114
136
  root = findViewById(R.id.native_root)
115
- val config = runCatching {
137
+ config = runCatching {
116
138
  assets.open("craft.config.json").bufferedReader().use {
117
139
  JSONObject(it.readText())
118
140
  }
119
141
  }.getOrDefault(JSONObject())
120
142
  hapticsEnabled = config.optBoolean("enableHaptics", false)
143
+ clipboardEnabled = config.optBoolean("enableClipboard", false)
144
+ capabilities = CraftNativeCapabilities(this, config)
121
145
  val background = color(config.optString("backgroundColor", "#1a1a2e"), Color.BLACK)
122
146
  root.setBackgroundColor(background)
123
147
  defaultTextColor = if (Color.luminance(background) < 0.5) Color.WHITE else Color.BLACK
@@ -131,29 +155,45 @@ class MainActivity : AppCompatActivity() {
131
155
  showError("Native STX requires Android JavaScriptSandbox, which is unavailable on this device.")
132
156
  return
133
157
  }
134
- val connection = JavaScriptSandbox.createConnectedInstanceAsync(this)
135
- connection.addListener({
136
- try {
137
- val engine = connection.get()
138
- if (isDestroyed) {
139
- engine.close()
140
- return@addListener
141
- }
142
- useMessagePorts = engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_MESSAGE_PORTS)
143
- && engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)
144
- if (!useMessagePorts && !engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING)) {
145
- engine.close()
146
- showError("Native STX requires JavaScript message ports or console messaging, unavailable on this device.")
147
- return@addListener
148
- }
149
- Log.i(TAG, "Native STX bridge: ${if (useMessagePorts) "message ports" else "console callback"}")
150
- sandbox = engine
151
- openScreen(null)
152
- } catch (error: Exception) {
153
- Log.e(TAG, "JavaScriptSandbox connection failed", error)
154
- showError("Native STX JavaScript engine could not start: ${error.message}")
158
+ val connection = synchronized(SANDBOX_LOCK) {
159
+ sharedSandboxConnection ?: sharedSandbox?.let { existing ->
160
+ sandbox = existing
161
+ null
162
+ } ?: JavaScriptSandbox.createConnectedInstanceAsync(applicationContext).also {
163
+ sharedSandboxConnection = it
155
164
  }
156
- }, ContextCompat.getMainExecutor(this))
165
+ }
166
+ if (connection == null) {
167
+ sandbox?.let(::attachSandbox)
168
+ } else {
169
+ connection.addListener({
170
+ try {
171
+ val engine = connection.get()
172
+ synchronized(SANDBOX_LOCK) {
173
+ sharedSandbox = engine
174
+ }
175
+ if (!isDestroyed) {
176
+ sandbox = engine
177
+ attachSandbox(engine)
178
+ }
179
+ } catch (error: Exception) {
180
+ synchronized(SANDBOX_LOCK) { sharedSandboxConnection = null }
181
+ Log.e(TAG, "JavaScriptSandbox connection failed", error)
182
+ if (!isDestroyed) showError("Native STX JavaScript engine could not start: ${error.message}")
183
+ }
184
+ }, ContextCompat.getMainExecutor(this))
185
+ }
186
+ }
187
+
188
+ private fun attachSandbox(engine: JavaScriptSandbox) {
189
+ useMessagePorts = engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_MESSAGE_PORTS)
190
+ && engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN)
191
+ if (!useMessagePorts && !engine.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING)) {
192
+ showError("Native STX requires JavaScript message ports or console messaging, unavailable on this device.")
193
+ return
194
+ }
195
+ Log.i(TAG, "Native STX bridge: ${if (useMessagePorts) "message ports" else "console callback"}")
196
+ openScreen(null)
157
197
  }
158
198
 
159
199
  private fun openScreen(route: String?, params: JSONObject = JSONObject()) {
@@ -170,13 +210,13 @@ class MainActivity : AppCompatActivity() {
170
210
  val port = if (useMessagePorts) {
171
211
  isolate.createMessageChannel("craft", executor, MessagePortClient { message ->
172
212
  val current = screen ?: return@MessagePortClient
173
- if (current.active && message.type == Message.TYPE_STRING) receive(current, message.string)
213
+ if (current.active && message.type == Message.TYPE_STRING) receiveSafely(current, message.string)
174
214
  })
175
215
  } else {
176
216
  isolate.setConsoleCallback(executor, JavaScriptConsoleCallback { entry ->
177
217
  val current = screen ?: return@JavaScriptConsoleCallback
178
218
  if (current.active && entry.message.startsWith(prefix)) {
179
- receive(current, entry.message.removePrefix(prefix))
219
+ receiveSafely(current, entry.message.removePrefix(prefix))
180
220
  }
181
221
  })
182
222
  null
@@ -194,7 +234,12 @@ class MainActivity : AppCompatActivity() {
194
234
  const port = await android.getNamedPort('craft');
195
235
  $routePrelude
196
236
  globalThis.__stxNativeBridge = {
237
+ platform: 'android',
197
238
  mutationProtocolVersion: 1,
239
+ capabilityProtocolVersion: $CRAFT_NATIVE_CAPABILITY_PROTOCOL_VERSION,
240
+ capabilityTimeoutMs: $CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS,
241
+ capabilities: ${capabilities.names()},
242
+ initialAppState: ${JSONObject.quote(nativeAppState)},
198
243
  postMessage: function(value) { port.postMessage(value); },
199
244
  onMessage: function(callback) { port.onmessage = function(event) { callback(event.data); }; }
200
245
  };
@@ -202,7 +247,12 @@ class MainActivity : AppCompatActivity() {
202
247
  """.trimIndent() else """
203
248
  $routePrelude
204
249
  globalThis.__stxNativeBridge = {
250
+ platform: 'android',
205
251
  mutationProtocolVersion: 1,
252
+ capabilityProtocolVersion: $CRAFT_NATIVE_CAPABILITY_PROTOCOL_VERSION,
253
+ capabilityTimeoutMs: $CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS,
254
+ capabilities: ${capabilities.names()},
255
+ initialAppState: ${JSONObject.quote(nativeAppState)},
206
256
  postMessage: function(value) { console.log(${JSONObject.quote(prefix)} + value); },
207
257
  onMessage: function(callback) { globalThis.__stxNativeCallback = callback; }
208
258
  };
@@ -214,7 +264,18 @@ class MainActivity : AppCompatActivity() {
214
264
  setup.get()
215
265
  val load = isolate.evaluateJavaScriptAsync(bundle)
216
266
  load.addListener({
217
- try { load.get() } catch (error: Exception) {
267
+ try {
268
+ load.get()
269
+ val launchLink = initialDeepLink
270
+ if (!deliveredInitialDeepLink
271
+ && config.optBoolean("enableDeepLinks")
272
+ && launchLink != null
273
+ && screens.lastOrNull() === activeScreen
274
+ ) {
275
+ deliveredInitialDeepLink = true
276
+ send(activeScreen, "DEEP_LINK", deepLinkData(launchLink).put("initial", true))
277
+ }
278
+ } catch (error: Exception) {
218
279
  Log.e(TAG, "Native STX bundle failed", error)
219
280
  if (activeScreen.active) showError("Native STX bundle failed: ${error.message}")
220
281
  }
@@ -232,9 +293,17 @@ class MainActivity : AppCompatActivity() {
232
293
  root.addView(screen.view)
233
294
  }
234
295
 
296
+ override fun onSaveInstanceState(outState: Bundle) {
297
+ outState.putString("craft.initialDeepLink", initialDeepLink?.toString())
298
+ outState.putBoolean("craft.initialDeepLinkClaimed", initialDeepLinkClaimed)
299
+ outState.putBoolean("craft.deliveredInitialDeepLink", deliveredInitialDeepLink)
300
+ super.onSaveInstanceState(outState)
301
+ }
302
+
235
303
  private fun receive(screen: Screen, raw: String) {
236
304
  val message = runCatching { JSONObject(raw) }.getOrNull() ?: return
237
305
  val payload = message.optJSONObject("payload") ?: JSONObject()
306
+ Log.d(TAG, "Native STX ${message.optString("type")} (${raw.length} chars) for ${screen.route ?: "home"}")
238
307
  when (message.optString("type")) {
239
308
  "RENDER" -> payload.optJSONObject("document")?.let { render(screen, it) }
240
309
  "MUTATE" -> try {
@@ -270,6 +339,26 @@ class MainActivity : AppCompatActivity() {
270
339
  }
271
340
  "NAVIGATE_BACK" -> if (screens.lastOrNull() === screen) back()
272
341
  "API_REQUEST" -> answerAPI(screen, message, payload)
342
+ "API_CANCEL" -> {
343
+ val id = payload.optString("requestId")
344
+ if (id.isNotBlank()) {
345
+ val token = "${screen.capabilityScope}/$id"
346
+ screen.pendingCapabilities.remove(token)
347
+ screen.capabilityTimeouts.remove(token)?.let(capabilityHandler::removeCallbacks)
348
+ capabilities.cancel(token)
349
+ }
350
+ }
351
+ }
352
+ }
353
+
354
+ private fun receiveSafely(screen: Screen, raw: String) {
355
+ try {
356
+ receive(screen, raw)
357
+ }
358
+ catch (error: Exception) {
359
+ val route = screen.route ?: "home"
360
+ Log.e(TAG, "Native STX message failed for $route", error)
361
+ if (screen.active) showError("Native STX message failed for $route: ${error.message}")
273
362
  }
274
363
  }
275
364
 
@@ -728,34 +817,118 @@ class MainActivity : AppCompatActivity() {
728
817
  val id = message.optString("id")
729
818
  if (id.isBlank()) return
730
819
  val args = payload.optJSONArray("args") ?: JSONArray()
731
- val response = JSONObject().put("requestId", id)
732
- val result = when (payload.optString("module") to payload.optString("method")) {
733
- "Device" to "getInfo" -> response.put("data", JSONObject()
820
+ val token = "${screen.capabilityScope}/$id"
821
+ screen.pendingCapabilities.add(token)
822
+ val timeout = Runnable {
823
+ if (!screen.active || !screen.pendingCapabilities.remove(token)) return@Runnable
824
+ screen.capabilityTimeouts.remove(token)
825
+ capabilities.cancel(token)
826
+ send(screen, "API_ERROR", JSONObject()
827
+ .put("version", CRAFT_NATIVE_CAPABILITY_PROTOCOL_VERSION)
828
+ .put("requestId", id)
829
+ .put("code", "TIMEOUT")
830
+ .put("message", "Native API request timed out"), id)
831
+ }
832
+ screen.capabilityTimeouts[token] = timeout
833
+ capabilityHandler.postDelayed(timeout, CRAFT_NATIVE_CAPABILITY_TIMEOUT_MS)
834
+ capabilities.perform(
835
+ token,
836
+ payload.optInt("version", CRAFT_NATIVE_CAPABILITY_PROTOCOL_VERSION),
837
+ payload.optString("module"),
838
+ payload.optString("method"),
839
+ args,
840
+ ) { result ->
841
+ if (!screen.active || !screen.pendingCapabilities.remove(token)) return@perform
842
+ screen.capabilityTimeouts.remove(token)?.let(capabilityHandler::removeCallbacks)
843
+ val response = JSONObject()
844
+ .put("version", CRAFT_NATIVE_CAPABILITY_PROTOCOL_VERSION)
845
+ .put("requestId", id)
846
+ if (result.code == null) response.put("data", result.data ?: JSONObject.NULL)
847
+ else response.put("code", result.code).put("message", result.message ?: "Native API failed")
848
+ send(screen, if (result.code == null) "API_RESPONSE" else "API_ERROR", response, id)
849
+ }
850
+ }
851
+
852
+ internal fun performImmediateCapability(module: String, method: String, args: JSONArray): CraftNativeCapabilityResult =
853
+ when (module to method) {
854
+ "Device" to "getInfo" -> CraftNativeCapabilityResult(data = JSONObject()
734
855
  .put("platform", "android")
735
856
  .put("osVersion", Build.VERSION.RELEASE)
736
857
  .put("model", Build.MODEL)
737
- .put("manufacturer", Build.MANUFACTURER))
738
- "Clipboard" to "write" -> {
858
+ .put("manufacturer", Build.MANUFACTURER)
859
+ .put("deviceId", android.provider.Settings.Secure.getString(contentResolver, android.provider.Settings.Secure.ANDROID_ID))
860
+ .put("isTablet", resources.configuration.smallestScreenWidthDp >= 600)
861
+ .put("screen", JSONObject()
862
+ .put("width", resources.displayMetrics.widthPixels / resources.displayMetrics.density)
863
+ .put("height", resources.displayMetrics.heightPixels / resources.displayMetrics.density)
864
+ .put("scale", resources.displayMetrics.density)))
865
+ "Clipboard" to "write" -> if (!clipboardEnabled) {
866
+ CraftNativeCapabilityResult(code = "CAPABILITY_DISABLED", message = "Clipboard is disabled")
867
+ } else if (args.opt(0) !is String) {
868
+ CraftNativeCapabilityResult(code = "INVALID_ARGUMENT", message = "Clipboard.write needs text")
869
+ } else {
739
870
  val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
740
- clipboard.setPrimaryClip(ClipData.newPlainText("", args.optString(0)))
741
- response.put("data", JSONObject().put("ok", true))
871
+ clipboard.setPrimaryClip(ClipData.newPlainText("", args.getString(0)))
872
+ CraftNativeCapabilityResult(data = true)
742
873
  }
743
- "Clipboard" to "read" -> {
874
+ "Clipboard" to "read" -> if (!clipboardEnabled) {
875
+ CraftNativeCapabilityResult(code = "CAPABILITY_DISABLED", message = "Clipboard is disabled")
876
+ } else {
744
877
  val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
745
- response.put("data", clipboard.primaryClip?.getItemAt(0)?.coerceToText(this)?.toString() ?: "")
878
+ CraftNativeCapabilityResult(data = clipboard.primaryClip?.getItemAt(0)?.coerceToText(this)?.toString() ?: "")
746
879
  }
747
- "Haptics" to "impact" -> {
748
- if (!hapticsEnabled) response.put("code", "CAPABILITY_DISABLED")
749
- .put("message", "Haptics are disabled")
750
- else {
751
- val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
752
- vibrator.vibrate(VibrationEffect.createOneShot(30, VibrationEffect.DEFAULT_AMPLITUDE))
753
- response.put("data", JSONObject().put("ok", true))
754
- }
880
+ "Haptics" to "impact" -> if (!hapticsEnabled) {
881
+ CraftNativeCapabilityResult(code = "CAPABILITY_DISABLED", message = "Haptics are disabled")
882
+ } else {
883
+ val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
884
+ vibrator.vibrate(VibrationEffect.createOneShot(30, VibrationEffect.DEFAULT_AMPLITUDE))
885
+ CraftNativeCapabilityResult(data = true)
755
886
  }
756
- else -> response.put("code", "UNKNOWN_ACTION").put("message", "Unsupported native API")
887
+ else -> CraftNativeCapabilityResult(code = "UNKNOWN_ACTION", message = "Unsupported native API")
757
888
  }
758
- send(screen, if (result.has("code")) "API_ERROR" else "API_RESPONSE", result, id)
889
+
890
+ internal fun deepLinkData(uri: android.net.Uri): JSONObject = JSONObject()
891
+ .put("url", uri.toString())
892
+ .put("scheme", uri.scheme ?: "")
893
+ .put("host", uri.host ?: "")
894
+ .put("path", uri.path ?: "")
895
+ .put("query", uri.query ?: "")
896
+ .put("queryParams", JSONObject().apply {
897
+ uri.queryParameterNames.forEach { name -> put(name, uri.getQueryParameter(name) ?: "") }
898
+ })
899
+
900
+ private fun publish(type: String, payload: JSONObject) {
901
+ screens.lastOrNull()?.takeIf { it.active }?.let { send(it, type, payload) }
902
+ }
903
+
904
+ override fun onStart() {
905
+ super.onStart()
906
+ nativeAppState = "active"
907
+ publish("APP_STATE", JSONObject().put("state", nativeAppState))
908
+ }
909
+
910
+ override fun onPause() {
911
+ nativeAppState = "inactive"
912
+ publish("APP_STATE", JSONObject().put("state", nativeAppState))
913
+ super.onPause()
914
+ }
915
+
916
+ override fun onStop() {
917
+ nativeAppState = "background"
918
+ publish("APP_STATE", JSONObject().put("state", nativeAppState))
919
+ super.onStop()
920
+ }
921
+
922
+ override fun onNewIntent(intent: Intent) {
923
+ super.onNewIntent(intent)
924
+ setIntent(intent)
925
+ intent.data?.let { publish("DEEP_LINK", deepLinkData(it).put("initial", false)) }
926
+ }
927
+
928
+ internal fun consumeInitialDeepLink(): Uri? {
929
+ if (initialDeepLinkClaimed) return null
930
+ initialDeepLinkClaimed = true
931
+ return initialDeepLink
759
932
  }
760
933
 
761
934
  private fun send(screen: Screen, type: String, payload: JSONObject, correlationId: String? = null) {
@@ -804,6 +977,10 @@ class MainActivity : AppCompatActivity() {
804
977
  }
805
978
 
806
979
  private fun closeScreen(screen: Screen) {
980
+ screen.capabilityTimeouts.values.forEach(capabilityHandler::removeCallbacks)
981
+ screen.capabilityTimeouts.clear()
982
+ screen.pendingCapabilities.forEach(capabilities::cancel)
983
+ screen.pendingCapabilities.clear()
807
984
  releaseViewState(screen, retainedViews(screen).toList())
808
985
  screen.controls.clear()
809
986
  screen.handlers.clear()
@@ -833,9 +1010,14 @@ class MainActivity : AppCompatActivity() {
833
1010
  imageJobs.values.forEach { it.cancel(true) }
834
1011
  imageJobs.clear()
835
1012
  imageExecutor.shutdownNow()
836
- sandbox?.close()
1013
+ capabilities.close()
837
1014
  super.onDestroy()
838
1015
  }
839
1016
 
840
- companion object { private const val TAG = "CraftNativeAndroid" }
1017
+ companion object {
1018
+ private const val TAG = "CraftNativeAndroid"
1019
+ private val SANDBOX_LOCK = Any()
1020
+ private var sharedSandbox: JavaScriptSandbox? = null
1021
+ private var sharedSandboxConnection: ListenableFuture<JavaScriptSandbox>? = null
1022
+ }
841
1023
  }
@@ -352,7 +352,7 @@ export declare const camera: {
352
352
  /**
353
353
  * Biometric type.
354
354
  */
355
- export type BiometricType = 'faceId' | 'touchId' | 'fingerprint' | 'face' | 'iris';
355
+ export type BiometricType = 'faceId' | 'touchId' | 'fingerprint' | 'face' | 'iris' | 'none';
356
356
  /**
357
357
  * Biometric authentication API.
358
358
  *
@@ -433,6 +433,15 @@ export declare const secureStorage: {
433
433
  * @param key - Storage key
434
434
  */
435
435
  delete(key: string): Promise<void>;
436
+ /**
437
+ * Remove a securely stored value using the legacy native contract.
438
+ *
439
+ * New code may use {@link delete}; this alias keeps older native screens
440
+ * and the public `craft.d.ts` contract working across bridge versions.
441
+ *
442
+ * @param key - Storage key
443
+ */
444
+ remove(key: string): Promise<void>;
436
445
  /**
437
446
  * Clear all securely stored values.
438
447
  */
@@ -621,6 +630,12 @@ export interface NotificationOptions {
621
630
  export type BridgeNotification = Omit<NotificationOptions, 'scheduleAt'> & {
622
631
  delay?: number;
623
632
  };
633
+ /** A local notification waiting for delivery. */
634
+ export interface PendingLocalNotification {
635
+ id: string;
636
+ title: string;
637
+ body: string;
638
+ }
624
639
  /**
625
640
  * The bridges' spelling of a notification: `scheduleAt` becomes `delay`.
626
641
  *
@@ -668,6 +683,14 @@ export declare const notifications: {
668
683
  * @param options - Notification options with scheduleAt
669
684
  */
670
685
  schedule(options: NotificationOptions): Promise<void>;
686
+ /**
687
+ * Cancel one scheduled notification.
688
+ */
689
+ cancel(id: string): Promise<void>;
690
+ /**
691
+ * Read notifications that have not fired yet.
692
+ */
693
+ pending(): Promise<PendingLocalNotification[]>;
671
694
  /**
672
695
  * Cancel all scheduled notifications.
673
696
  */
@@ -687,6 +710,34 @@ export declare const keepAwake: {
687
710
  enable(): Promise<void>;
688
711
  disable(): Promise<void>;
689
712
  };
713
+ export interface SpeechOptions {
714
+ /** Speaking rate, 0.5 to 2. 1 is the voice's normal pace. */
715
+ rate?: number;
716
+ /** BCP-47 language tag, such as 'en-US'. Defaults to the device's language. */
717
+ language?: string;
718
+ /** Stop whatever is being said first. Defaults to true; false queues behind it. */
719
+ interrupt?: boolean;
720
+ }
721
+ /**
722
+ * Short spoken cues: "Rest, 15 seconds. Up next: Dead Bug".
723
+ *
724
+ * Inside a Craft app this is native text-to-speech, which ducks the person's
725
+ * music rather than stopping it and, on iOS, plays with the silent switch on.
726
+ * In a browser it falls back to the Web Speech API, which does neither, and
727
+ * where there is no speech at all it answers `false` rather than throwing, so
728
+ * a cue never breaks the flow around it.
729
+ */
730
+ export declare const speech: {
731
+ isAvailable(): boolean;
732
+ /**
733
+ * Speak `text`. Resolves when the utterance ends: `true` when it was
734
+ * spoken, `false` when it was stopped, interrupted or could not be spoken.
735
+ * Rejects with code `INVALID_ARGUMENT` when `text` is empty.
736
+ */
737
+ speak(text: string, options?: SpeechOptions): Promise<boolean>;
738
+ /** Stop speaking at once. Every pending `speak()` resolves `false`. */
739
+ stop(): Promise<void>;
740
+ };
690
741
  export declare const deepLinks: {
691
742
  getInitialURL(): Promise<string | null>;
692
743
  /**
@@ -852,6 +903,7 @@ declare const mobile: {
852
903
  lifecycle: typeof lifecycle;
853
904
  notifications: typeof notifications;
854
905
  keepAwake: typeof keepAwake;
906
+ speech: typeof speech;
855
907
  deepLinks: typeof deepLinks;
856
908
  network: typeof network;
857
909
  appReview: typeof appReview;
package/dist/cli.js CHANGED
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3303
3303
  `);
3304
3304
  }
3305
3305
  // package.json
3306
- var version = "0.0.115";
3306
+ var version = "0.0.119";
3307
3307
 
3308
3308
  // bin/cli.ts
3309
3309
  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.115";
313
+ var version = "0.0.119";
314
314
 
315
315
  // src/sdk-version.ts
316
316
  var SDK_VERSION = version;
@@ -5072,6 +5072,17 @@ var secureStorage = {
5072
5072
  throw new Error("Craft secure storage bridge is unavailable");
5073
5073
  localStorage.removeItem(`secure_${key}`);
5074
5074
  },
5075
+ async remove(key) {
5076
+ const craft = getCraftMobile();
5077
+ if (typeof window !== "undefined" && craft?.secureStorage) {
5078
+ if (typeof craft.secureStorage.remove === "function")
5079
+ return craft.secureStorage.remove(key);
5080
+ return craft.secureStorage.delete(key);
5081
+ }
5082
+ if (typeof window !== "undefined" && window.craft)
5083
+ throw new Error("Craft secure storage bridge is unavailable");
5084
+ localStorage.removeItem(`secure_${key}`);
5085
+ },
5075
5086
  async clear() {
5076
5087
  const craft = getCraftMobile();
5077
5088
  if (typeof window !== "undefined" && craft?.secureStorage) {
@@ -5271,6 +5282,20 @@ var notifications = {
5271
5282
  }
5272
5283
  throw new Error("Scheduled notifications not available in web");
5273
5284
  },
5285
+ async cancel(id) {
5286
+ const craft = getCraftMobile();
5287
+ if (typeof window !== "undefined" && craft?.notifications) {
5288
+ return craft.notifications.cancel(id);
5289
+ }
5290
+ throw new Error("Scheduled notifications not available in web");
5291
+ },
5292
+ async pending() {
5293
+ const craft = getCraftMobile();
5294
+ if (typeof window !== "undefined" && craft?.notifications) {
5295
+ return craft.notifications.pending();
5296
+ }
5297
+ return [];
5298
+ },
5274
5299
  async cancelAll() {
5275
5300
  const craft = getCraftMobile();
5276
5301
  if (typeof window !== "undefined" && craft?.notifications) {
@@ -5284,6 +5309,7 @@ var notifications = {
5284
5309
  }
5285
5310
  }
5286
5311
  };
5312
+ var speakingOnTheWeb = new Set;
5287
5313
  function detectPlatform2() {
5288
5314
  if (typeof window === "undefined")
5289
5315
  return "linux";
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.115";
61
+ var version = "0.0.119";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -4832,6 +4832,17 @@ var secureStorage = {
4832
4832
  throw new Error("Craft secure storage bridge is unavailable");
4833
4833
  localStorage.removeItem(`secure_${key}`);
4834
4834
  },
4835
+ async remove(key) {
4836
+ const craft = getCraftMobile();
4837
+ if (typeof window !== "undefined" && craft?.secureStorage) {
4838
+ if (typeof craft.secureStorage.remove === "function")
4839
+ return craft.secureStorage.remove(key);
4840
+ return craft.secureStorage.delete(key);
4841
+ }
4842
+ if (typeof window !== "undefined" && window.craft)
4843
+ throw new Error("Craft secure storage bridge is unavailable");
4844
+ localStorage.removeItem(`secure_${key}`);
4845
+ },
4835
4846
  async clear() {
4836
4847
  const craft = getCraftMobile();
4837
4848
  if (typeof window !== "undefined" && craft?.secureStorage) {
@@ -5031,6 +5042,20 @@ var notifications = {
5031
5042
  }
5032
5043
  throw new Error("Scheduled notifications not available in web");
5033
5044
  },
5045
+ async cancel(id) {
5046
+ const craft = getCraftMobile();
5047
+ if (typeof window !== "undefined" && craft?.notifications) {
5048
+ return craft.notifications.cancel(id);
5049
+ }
5050
+ throw new Error("Scheduled notifications not available in web");
5051
+ },
5052
+ async pending() {
5053
+ const craft = getCraftMobile();
5054
+ if (typeof window !== "undefined" && craft?.notifications) {
5055
+ return craft.notifications.pending();
5056
+ }
5057
+ return [];
5058
+ },
5034
5059
  async cancelAll() {
5035
5060
  const craft = getCraftMobile();
5036
5061
  if (typeof window !== "undefined" && craft?.notifications) {
@@ -5044,6 +5069,7 @@ var notifications = {
5044
5069
  }
5045
5070
  }
5046
5071
  };
5072
+ var speakingOnTheWeb = new Set;
5047
5073
  function detectPlatform2() {
5048
5074
  if (typeof window === "undefined")
5049
5075
  return "linux";
@@ -66,6 +66,22 @@ export interface CraftConfig {
66
66
  teamId?: string;
67
67
  urlSchemes?: string[];
68
68
  trustedOrigins?: string[];
69
+ /**
70
+ * The domains the app's web view treats as its own (WKAppBoundDomains, at
71
+ * most 10). Service workers only run in an iOS web view on these, so a site
72
+ * that works offline in Safari works offline in the app. Unset, they are the
73
+ * hosts of `trustedOrigins` and the dev server: the origins the bridge is
74
+ * already injected into, and once any are declared iOS keeps script
75
+ * injection to them. `[]` declares none.
76
+ */
77
+ appBoundDomains?: string[];
78
+ /**
79
+ * Also refuse to load anything but the app-bound domains in the web view
80
+ * (WKWebViewConfiguration.limitsNavigationsToAppBoundDomains). Off by
81
+ * default: links elsewhere already open in Safari, and embedded players and
82
+ * maps in iframes keep loading.
83
+ */
84
+ limitNavigationsToAppBoundDomains?: boolean;
69
85
  associatedDomains?: string[];
70
86
  appGroups?: string[];
71
87
  appIconPath?: string;
@@ -148,6 +164,8 @@ export declare function renderAppearance(config: Pick<CraftConfig, 'appearance'
148
164
  };
149
165
  export declare function renderUsageDescriptions(config: CraftConfig): string;
150
166
  export declare function renderOrientations(config: CraftConfig): string;
167
+ /** WKAppBoundDomains: the configured domains plus the dev server's host, at most 10. */
168
+ export declare function renderAppBoundDomains(config: CraftConfig): string;
151
169
  export declare function renderUrlTypes(config: CraftConfig): string;
152
170
  export declare function renderBackgroundModes(config: CraftConfig): string;
153
171
  export declare function renderEntitlements(config: CraftConfig): string;