craft-native 0.0.200 → 0.0.201
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.d.ts +8 -0
- package/dist/android/src/index.js +31 -3
- package/dist/android/templates/AndroidManifest.xml.template +6 -0
- package/dist/android/templates/CraftAppLinks.kt.template +47 -0
- package/dist/android/templates/CraftNativeServices.kt.template +1 -2
- package/dist/android/templates/MainActivity.kt.template +11 -0
- package/dist/android/templates/MainActivityNative.kt.template +9 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -50,6 +50,14 @@ export interface CraftAndroidConfig {
|
|
|
50
50
|
/** Reach the person's own devices on the local network over plain HTTP (a Mac companion). */
|
|
51
51
|
enableLocalNetwork?: boolean;
|
|
52
52
|
urlSchemes?: string[];
|
|
53
|
+
/** HTTPS hosts owned by the app; path rules are ordered, with `*` matching any path text. */
|
|
54
|
+
appLinks?: Array<{
|
|
55
|
+
host: string;
|
|
56
|
+
paths?: Array<{
|
|
57
|
+
pattern: string;
|
|
58
|
+
exclude?: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
}>;
|
|
53
61
|
trustedOrigins?: string[];
|
|
54
62
|
appIconPath?: string;
|
|
55
63
|
googleServicesFile?: string;
|
|
@@ -188,6 +188,7 @@ var DEFAULT_CONFIG = {
|
|
|
188
188
|
enableKeepAwake: false,
|
|
189
189
|
enableDeepLinks: false,
|
|
190
190
|
urlSchemes: [],
|
|
191
|
+
appLinks: [],
|
|
191
192
|
trustedOrigins: [],
|
|
192
193
|
minSdk: ANDROID_MIN_SDK,
|
|
193
194
|
compileSdk: 36,
|
|
@@ -255,8 +256,21 @@ function normalizeAndroidNetworkConfig(config) {
|
|
|
255
256
|
throw new Error("Android deep-link schemes must be valid URI schemes");
|
|
256
257
|
}
|
|
257
258
|
config.urlSchemes = [...new Set(schemes)];
|
|
258
|
-
|
|
259
|
-
|
|
259
|
+
config.appLinks = (config.appLinks ?? []).map((link) => {
|
|
260
|
+
const host = link.host.trim().toLowerCase();
|
|
261
|
+
const parsed = new URL(`https://${host}`);
|
|
262
|
+
if (!host || parsed.hostname !== host || parsed.port || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash)
|
|
263
|
+
throw new Error("Android App Links need a hostname without a scheme, port or path");
|
|
264
|
+
for (const rule of link.paths ?? []) {
|
|
265
|
+
if (!rule.pattern.startsWith("/") || /[\x00-\x1f]/.test(rule.pattern) || rule.exclude !== undefined && typeof rule.exclude !== "boolean")
|
|
266
|
+
throw new Error("Android App Link path rules need an absolute path pattern and a boolean exclude");
|
|
267
|
+
}
|
|
268
|
+
return { ...link, host };
|
|
269
|
+
});
|
|
270
|
+
if (new Set(config.appLinks.map((link) => link.host)).size !== config.appLinks.length)
|
|
271
|
+
throw new Error("Android App Link hosts must be unique");
|
|
272
|
+
if (config.enableDeepLinks && config.urlSchemes.length === 0 && config.appLinks.length === 0) {
|
|
273
|
+
throw new Error("Android deep links require at least one URL scheme or an App Link host");
|
|
260
274
|
}
|
|
261
275
|
const trustedOrigins = (config.trustedOrigins ?? []).map((value) => {
|
|
262
276
|
return androidWebUrl(value, "Android trusted origin").origin;
|
|
@@ -454,12 +468,25 @@ function renderAndroidDeepLinks(config) {
|
|
|
454
468
|
if (!config.enableDeepLinks)
|
|
455
469
|
return "";
|
|
456
470
|
const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter((value) => /^[a-z][a-z0-9+.-]*$/i.test(value)) ?? [])];
|
|
457
|
-
|
|
471
|
+
const custom = schemes.map((scheme) => ` <intent-filter>
|
|
458
472
|
<action android:name="android.intent.action.VIEW" />
|
|
459
473
|
<category android:name="android.intent.category.DEFAULT" />
|
|
460
474
|
<category android:name="android.intent.category.BROWSABLE" />
|
|
461
475
|
<data android:scheme="${scheme}" />
|
|
462
476
|
</intent-filter>`).join(`
|
|
477
|
+
`);
|
|
478
|
+
const verified = (config.appLinks ?? []).map((link) => {
|
|
479
|
+
if (!/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i.test(link.host))
|
|
480
|
+
throw new Error("Invalid Android App Link host");
|
|
481
|
+
return ` <intent-filter android:autoVerify="true">
|
|
482
|
+
<action android:name="android.intent.action.VIEW" />
|
|
483
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
484
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
485
|
+
<data android:scheme="https" android:host="${link.host}" />
|
|
486
|
+
</intent-filter>`;
|
|
487
|
+
}).join(`
|
|
488
|
+
`);
|
|
489
|
+
return [custom, verified].filter(Boolean).join(`
|
|
463
490
|
`);
|
|
464
491
|
}
|
|
465
492
|
var RUNTIME_ABIS = ["arm64-v8a", "x86_64"];
|
|
@@ -555,6 +582,7 @@ async function init(options) {
|
|
|
555
582
|
const mainActivityTemplate = readFileSync(join(TEMPLATES_DIR, config.renderer === "native" ? "MainActivityNative.kt.template" : "MainActivity.kt.template"), "utf-8");
|
|
556
583
|
const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
|
|
557
584
|
writeFileSync(join(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
|
|
585
|
+
writeFileSync(join(output, "app/src/main/java", packagePath, "CraftAppLinks.kt"), readFileSync(join(TEMPLATES_DIR, "CraftAppLinks.kt.template"), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
558
586
|
writeFileSync(join(output, "app/src/main/java", packagePath, "CraftCrashReporter.kt"), readFileSync(join(TEMPLATES_DIR, "CraftCrashReporter.kt.template"), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_ANDROID_VERSION));
|
|
559
587
|
if (config.renderer === "native") {
|
|
560
588
|
const mutationTemplate = readFileSync(join(TEMPLATES_DIR, "CraftNativeMutation.kt.template"), "utf-8");
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
<!-- craft.speech: from Android 11 an app cannot see, and so cannot bind,
|
|
9
9
|
a text-to-speech engine it has not declared it will look for. -->
|
|
10
10
|
<queries>
|
|
11
|
+
<intent><action android:name="android.support.customtabs.action.CustomTabsService" /></intent>
|
|
12
|
+
<intent>
|
|
13
|
+
<action android:name="android.intent.action.VIEW" />
|
|
14
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
15
|
+
<data android:scheme="https" />
|
|
16
|
+
</intent>
|
|
11
17
|
<intent>
|
|
12
18
|
<action android:name="android.intent.action.TTS_SERVICE" />
|
|
13
19
|
</intent>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import androidx.browser.customtabs.CustomTabsClient
|
|
7
|
+
import androidx.browser.customtabs.CustomTabsIntent
|
|
8
|
+
import org.json.JSONObject
|
|
9
|
+
|
|
10
|
+
/** Ordered website path rules, also publishable as Android 15 dynamic App Links. */
|
|
11
|
+
internal object CraftAppLinks {
|
|
12
|
+
fun opensInBrowser(config: JSONObject?, uri: Uri): Boolean {
|
|
13
|
+
if (uri.scheme != "https") return false
|
|
14
|
+
val hosts = config?.optJSONArray("appLinks") ?: return false
|
|
15
|
+
for (index in 0 until hosts.length()) {
|
|
16
|
+
val host = hosts.optJSONObject(index) ?: continue
|
|
17
|
+
if (!host.optString("host").equals(uri.host, ignoreCase = true)) continue
|
|
18
|
+
val rules = host.optJSONArray("paths") ?: return false
|
|
19
|
+
val path = uri.path.orEmpty().ifEmpty { "/" }
|
|
20
|
+
for (ruleIndex in 0 until rules.length()) {
|
|
21
|
+
val rule = rules.optJSONObject(ruleIndex) ?: continue
|
|
22
|
+
val pattern = rule.optString("pattern")
|
|
23
|
+
val regex = pattern.split('*').joinToString(".*") { Regex.escape(it) }
|
|
24
|
+
if (Regex("^$regex$").matches(path)) return rule.optBoolean("exclude", false)
|
|
25
|
+
}
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** An explicit browser package avoids handing a verified site URL back to ourselves. */
|
|
32
|
+
fun openBrowser(activity: Activity, uri: Uri) {
|
|
33
|
+
val customTabsPackage = CustomTabsClient.getPackageName(activity, null)
|
|
34
|
+
if (customTabsPackage != null && customTabsPackage != activity.packageName) {
|
|
35
|
+
val tab = CustomTabsIntent.Builder().setShowTitle(true).build()
|
|
36
|
+
tab.intent.setPackage(customTabsPackage)
|
|
37
|
+
tab.launchUrl(activity, uri)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
val probe = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com")).addCategory(Intent.CATEGORY_BROWSABLE)
|
|
41
|
+
val browser = activity.packageManager.queryIntentActivities(probe, 0)
|
|
42
|
+
.firstOrNull { it.activityInfo.packageName != activity.packageName }
|
|
43
|
+
?.activityInfo?.packageName
|
|
44
|
+
?: throw IllegalStateException("No browser is installed")
|
|
45
|
+
activity.startActivity(Intent(Intent.ACTION_VIEW, uri).addCategory(Intent.CATEGORY_BROWSABLE).setPackage(browser))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -17,7 +17,6 @@ import android.speech.tts.UtteranceProgressListener
|
|
|
17
17
|
import android.util.Log
|
|
18
18
|
import android.view.WindowManager
|
|
19
19
|
import androidx.appcompat.app.AlertDialog
|
|
20
|
-
import androidx.browser.customtabs.CustomTabsIntent
|
|
21
20
|
import androidx.core.app.ActivityCompat
|
|
22
21
|
import androidx.core.app.NotificationManagerCompat
|
|
23
22
|
import androidx.core.content.ContextCompat
|
|
@@ -204,7 +203,7 @@ internal class CraftNativeServices(
|
|
|
204
203
|
pendingBrowser = callback to done
|
|
205
204
|
browserOpened = true
|
|
206
205
|
try {
|
|
207
|
-
|
|
206
|
+
CraftAppLinks.openBrowser(host, uri)
|
|
208
207
|
} catch (_: ActivityNotFoundException) {
|
|
209
208
|
host.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
|
210
209
|
}
|
|
@@ -99,6 +99,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
99
99
|
// reading its intent, which onNewIntent may have replaced with a
|
|
100
100
|
// later warm link.
|
|
101
101
|
val launchedFromHistory = intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0
|
|
102
|
+
if (savedInstanceState == null && !launchedFromHistory && intent.data?.let { CraftAppLinks.opensInBrowser(configJson(), it) } == true) {
|
|
103
|
+
CraftAppLinks.openBrowser(this, intent.data!!)
|
|
104
|
+
finish()
|
|
105
|
+
return
|
|
106
|
+
}
|
|
102
107
|
if (savedInstanceState == null) {
|
|
103
108
|
if (!launchedFromHistory) {
|
|
104
109
|
intent.data?.toString()?.let(craftBridge::receiveDeepLink)
|
|
@@ -300,6 +305,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
300
305
|
override fun onNewIntent(intent: Intent) {
|
|
301
306
|
super.onNewIntent(intent)
|
|
302
307
|
setIntent(intent)
|
|
308
|
+
intent.data?.let { link ->
|
|
309
|
+
if (CraftAppLinks.opensInBrowser(configJson(), link)) {
|
|
310
|
+
CraftAppLinks.openBrowser(this, link)
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
}
|
|
303
314
|
intent.data?.toString()?.let(craftBridge::receiveDeepLink)
|
|
304
315
|
handleIncomingShortcut(intent)
|
|
305
316
|
}
|
|
@@ -3458,6 +3458,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
3458
3458
|
routes = CraftNativeRoutes(config)
|
|
3459
3459
|
stores = CraftNativeStores.get(this, config)
|
|
3460
3460
|
services = CraftNativeServices(this, config) { this }
|
|
3461
|
+
if (savedInstanceState == null && intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == 0 && intent.data?.let { CraftAppLinks.opensInBrowser(config, it) } == true) {
|
|
3462
|
+
CraftAppLinks.openBrowser(this, intent.data!!)
|
|
3463
|
+
finish()
|
|
3464
|
+
return
|
|
3465
|
+
}
|
|
3461
3466
|
services.onBackgroundComplete = { ok, id ->
|
|
3462
3467
|
if (id == null) {
|
|
3463
3468
|
val waiting = backgroundWaiters.values.toList()
|
|
@@ -6182,6 +6187,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
6182
6187
|
}
|
|
6183
6188
|
intent.data?.let { link ->
|
|
6184
6189
|
if (::services.isInitialized && services.deepLink(link)) return
|
|
6190
|
+
if (CraftAppLinks.opensInBrowser(config, link)) {
|
|
6191
|
+
CraftAppLinks.openBrowser(this, link)
|
|
6192
|
+
return
|
|
6193
|
+
}
|
|
6185
6194
|
publish("DEEP_LINK", deepLinkData(link).put("initial", false))
|
|
6186
6195
|
if (::routes.isInitialized && !routes.isEmpty) routes.appPath(link.toString(), urlSchemes)?.let { navigateOpen(it, fromLink = true) }
|
|
6187
6196
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.201";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED