craft-native 0.0.118 → 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.
- package/dist/android/src/index.js +5 -1
- package/dist/android/templates/AndroidManifest.xml.template +1 -0
- package/dist/android/templates/CraftBridge.kt.template +3 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +455 -0
- package/dist/android/templates/CraftNativeFlatList.kt.template +27 -5
- package/dist/android/templates/CraftNativeNotificationReceiver.kt.template +43 -0
- package/dist/android/templates/MainActivityNative.kt.template +229 -47
- package/dist/api/mobile.d.ts +24 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +26 -1
- package/dist/index.js +26 -1
- package/dist/ios/src/index.d.ts +18 -0
- package/dist/ios/src/index.js +29 -1
- package/dist/ios/templates/CraftApp.swift +39 -1
- package/dist/ios/templates/CraftNativeActions.swift +472 -31
- package/dist/ios/templates/CraftNativeScreen.swift +93 -22
- package/dist/ios/templates/Info.plist.template +1 -0
- package/dist/mobile.js +25 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
}
|
|
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)
|
|
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
|
-
|
|
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 {
|
|
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
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
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.
|
|
741
|
-
|
|
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
|
-
|
|
878
|
+
CraftNativeCapabilityResult(data = clipboard.primaryClip?.getItemAt(0)?.coerceToText(this)?.toString() ?: "")
|
|
746
879
|
}
|
|
747
|
-
"Haptics" to "impact" -> {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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 ->
|
|
887
|
+
else -> CraftNativeCapabilityResult(code = "UNKNOWN_ACTION", message = "Unsupported native API")
|
|
757
888
|
}
|
|
758
|
-
|
|
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
|
-
|
|
1013
|
+
capabilities.close()
|
|
837
1014
|
super.onDestroy()
|
|
838
1015
|
}
|
|
839
1016
|
|
|
840
|
-
companion object {
|
|
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
|
}
|
package/dist/api/mobile.d.ts
CHANGED
|
@@ -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
|
*/
|
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.
|
|
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.
|
|
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) {
|
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.
|
|
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) {
|
package/dist/ios/src/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/ios/src/index.js
CHANGED
|
@@ -111,6 +111,27 @@ function renderOrientations(config) {
|
|
|
111
111
|
return values.map((value) => ` <string>${names[value]}</string>`).join(`
|
|
112
112
|
`);
|
|
113
113
|
}
|
|
114
|
+
function renderAppBoundDomains(config) {
|
|
115
|
+
const hostOf = (value) => {
|
|
116
|
+
try {
|
|
117
|
+
const url = new URL(value);
|
|
118
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url.hostname.toLowerCase() : null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const domains = config.appBoundDomains !== undefined ? config.appBoundDomains.map((value) => value.trim().toLowerCase()).filter(Boolean) : (config.trustedOrigins ?? []).map(hostOf).filter((host) => !!host);
|
|
124
|
+
if (!domains.length)
|
|
125
|
+
return "";
|
|
126
|
+
const devHost = config.devServerURL ? hostOf(config.devServerURL) : null;
|
|
127
|
+
if (devHost)
|
|
128
|
+
domains.push(devHost);
|
|
129
|
+
const unique = [...new Set(domains)].slice(0, 10);
|
|
130
|
+
return ` <key>WKAppBoundDomains</key>
|
|
131
|
+
<array>
|
|
132
|
+
${plistArray(unique)}
|
|
133
|
+
</array>`;
|
|
134
|
+
}
|
|
114
135
|
function renderUrlTypes(config) {
|
|
115
136
|
const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter(Boolean) ?? [])];
|
|
116
137
|
if (!schemes.length)
|
|
@@ -168,6 +189,12 @@ ${plistArray(config.appGroups)}
|
|
|
168
189
|
entries.push(` <key>aps-environment</key>
|
|
169
190
|
<string>$(CRAFT_APNS_ENVIRONMENT)</string>`);
|
|
170
191
|
}
|
|
192
|
+
if (config.enableSecureStorage) {
|
|
193
|
+
entries.push(` <key>keychain-access-groups</key>
|
|
194
|
+
<array>
|
|
195
|
+
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
|
|
196
|
+
</array>`);
|
|
197
|
+
}
|
|
171
198
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
172
199
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
173
200
|
<plist version="1.0">
|
|
@@ -410,7 +437,7 @@ async function init(options) {
|
|
|
410
437
|
cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
|
|
411
438
|
cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
|
|
412
439
|
const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
413
|
-
const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
|
|
440
|
+
const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
|
|
414
441
|
<true/>
|
|
415
442
|
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
|
|
416
443
|
<true/>` : "");
|
|
@@ -729,6 +756,7 @@ export {
|
|
|
729
756
|
renderDeviceFamilies,
|
|
730
757
|
renderBackgroundModes,
|
|
731
758
|
renderAppearance,
|
|
759
|
+
renderAppBoundDomains,
|
|
732
760
|
productName,
|
|
733
761
|
pickSimulator,
|
|
734
762
|
orderSimulators,
|