craft-native 0.0.238 → 0.0.241

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * Generates Android apps with native Craft components and optional WebView content.
5
5
  */
6
+ import { type AndroidMapProvider } from './native-map.js';
7
+ export type { AndroidMapProvider } from './native-map.js';
8
+ /** Providers this published builder can generate as real Android map views. */
9
+ export declare const nativeMapProviders: readonly ['google', 'maplibre'];
6
10
  export { runKotlinFixture } from './testing.js';
7
11
  export { createAndroidEmulator } from './emulator.js';
8
12
  export type { AndroidEmulatorOptions, AndroidEmulatorSession } from './emulator.js';
@@ -71,8 +75,20 @@ export interface CraftAndroidConfig {
71
75
  }>;
72
76
  trustedOrigins?: string[];
73
77
  appIconPath?: string;
78
+ /**
79
+ * The small icon in the status bar and on every notification the app
80
+ * shows, push or local: an Android vector drawable (`.xml`) or a `.png`,
81
+ * white on transparent, since Android draws only its shape. Without one,
82
+ * a white Craft mark. Never the launcher icon, which shows as a white
83
+ * square.
84
+ */
85
+ notificationIconPath?: string;
86
+ /** The `#RRGGBB` Android tints the notification icon and app name with. The system's own colour without it. */
87
+ notificationColor?: string;
74
88
  googleServicesFile?: string;
75
- /** Google Maps Android SDK key used by native `Map` screens. */
89
+ /** Native map renderer. MapLibre needs no SDK key; Google remains the default for compatibility. */
90
+ mapProvider?: AndroidMapProvider;
91
+ /** Google Maps Android SDK key, used only by the Google native map host. */
76
92
  mapsApiKey?: string;
77
93
  devServerURL?: string;
78
94
  hasBundledFallback?: boolean;
@@ -4,6 +4,48 @@ import { cpSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSy
4
4
  import { dirname as dirname2, extname, isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve2, sep } from "path";
5
5
  var {$ } = globalThis.Bun;
6
6
 
7
+ // ../android/src/native-map.ts
8
+ function androidMapHost(provider = "google") {
9
+ if (provider === "google")
10
+ return {
11
+ template: "CraftNativeMapGoogle.kt.template",
12
+ imports: `import com.google.android.gms.maps.CameraUpdateFactory
13
+ import com.google.android.gms.maps.GoogleMap
14
+ import com.google.android.gms.maps.MapView
15
+ import com.google.android.gms.maps.OnMapReadyCallback
16
+ import com.google.android.gms.maps.model.BitmapDescriptorFactory
17
+ import com.google.android.gms.maps.model.LatLng
18
+ import com.google.android.gms.maps.model.LatLngBounds
19
+ import com.google.android.gms.maps.model.Marker
20
+ import com.google.android.gms.maps.model.MarkerOptions
21
+ import com.google.android.gms.maps.model.Polyline
22
+ import com.google.android.gms.maps.model.PolylineOptions`,
23
+ dependency: "com.google.android.gms:play-services-maps:18.2.0"
24
+ };
25
+ if (provider === "maplibre")
26
+ return {
27
+ template: "CraftNativeMapLibre.kt.template",
28
+ imports: `import org.maplibre.android.MapLibre
29
+ import org.maplibre.android.camera.CameraPosition
30
+ import org.maplibre.android.camera.CameraUpdateFactory
31
+ import org.maplibre.android.geometry.LatLng
32
+ import org.maplibre.android.geometry.LatLngBounds
33
+ import org.maplibre.android.location.LocationComponentActivationOptions
34
+ import org.maplibre.android.maps.MapLibreMap
35
+ import org.maplibre.android.maps.MapView
36
+ import org.maplibre.android.maps.OnMapReadyCallback
37
+ import org.maplibre.android.maps.Style
38
+ import org.maplibre.android.style.expressions.Expression
39
+ import org.maplibre.android.style.layers.CircleLayer
40
+ import org.maplibre.android.style.layers.LineLayer
41
+ import org.maplibre.android.style.layers.SymbolLayer
42
+ import org.maplibre.android.style.layers.PropertyFactory.*
43
+ import org.maplibre.android.style.sources.GeoJsonSource`,
44
+ dependency: "org.maplibre.gl:android-sdk:13.7.1"
45
+ };
46
+ throw new Error(`Unknown Android native map provider: ${provider}. Expected google or maplibre.`);
47
+ }
48
+
7
49
  // ../android/src/promise-runtime.ts
8
50
  var ANDROID_PROMISE_RUNTIME = `
9
51
  if (window.__craftRejectPendingPromises) {
@@ -315,6 +357,7 @@ async function createAndroidEmulator(options) {
315
357
  }
316
358
 
317
359
  // ../android/src/index.ts
360
+ var nativeMapProviders = ["google", "maplibre"];
318
361
  function syncNativeFonts(fonts, output) {
319
362
  const destination = join3(output, "app/src/main/assets/NativeFonts");
320
363
  const names = new Set;
@@ -549,6 +592,7 @@ function normalizeAndroidNetworkConfig(config) {
549
592
  config.trustedOrigins = [...new Set(trustedOrigins)];
550
593
  }
551
594
  function validateAndroidConfig(config) {
595
+ androidMapHost(config.mapProvider);
552
596
  if (!config.appName.trim())
553
597
  throw new Error("Android app name must not be empty");
554
598
  if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
@@ -682,6 +726,7 @@ function writeAndroidConfig(output, config) {
682
726
  writeFileSync(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
683
727
  const runtimeConfig = { ...config };
684
728
  delete runtimeConfig.appIconPath;
729
+ delete runtimeConfig.notificationIconPath;
685
730
  delete runtimeConfig.googleServicesFile;
686
731
  writeFileSync(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
687
732
  }
@@ -802,7 +847,9 @@ function renderNativeShellManifest(config) {
802
847
  <intent-filter>
803
848
  <action android:name="com.google.firebase.MESSAGING_EVENT" />
804
849
  </intent-filter>
805
- </service>`);
850
+ </service>
851
+ <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/craft_notification_icon" />${config.notificationColor ? `
852
+ <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/craft_notification_color" />` : ""}`);
806
853
  }
807
854
  if (config.enableHealthConnect) {
808
855
  parts.push(` <activity-alias
@@ -912,6 +959,18 @@ async function init(options) {
912
959
  if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
913
960
  throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
914
961
  }
962
+ if (config.notificationIconPath)
963
+ requireRegularFile(config.notificationIconPath, "Notification icon");
964
+ const notificationIconExtension = config.notificationIconPath ? extname(config.notificationIconPath).toLowerCase() : undefined;
965
+ if (config.notificationIconPath && ![".png", ".xml"].includes(notificationIconExtension ?? "")) {
966
+ throw new Error(`Unsupported Android notification icon format: ${notificationIconExtension || "(none)"} (use a vector drawable .xml or a white-on-transparent .png)`);
967
+ }
968
+ if (config.notificationIconPath && notificationIconExtension === ".xml" && !/<vector[\s>]/.test(readFileSync2(config.notificationIconPath, "utf8"))) {
969
+ throw new Error("Notification icon .xml must be an Android vector drawable (<vector>)");
970
+ }
971
+ if (config.notificationColor !== undefined && !/^#[0-9a-f]{6}$/i.test(config.notificationColor)) {
972
+ throw new Error(`notificationColor must be #RRGGBB, not ${String(config.notificationColor)}`);
973
+ }
915
974
  const dirs = [
916
975
  output,
917
976
  join3(output, "app/src/main/java", packagePath),
@@ -936,7 +995,7 @@ async function init(options) {
936
995
  cpSync(config.googleServicesFile, join3(output, "app/google-services.json"));
937
996
  }
938
997
  const mainActivityTemplate = readFileSync2(join3(TEMPLATES_DIR, config.renderer === "native" ? "MainActivityNative.kt.template" : "MainActivity.kt.template"), "utf-8");
939
- const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
998
+ const mainActivity = mainActivityTemplate.replace(/\{\{NATIVE_MAP_IMPORTS\}\}/g, () => androidMapHost(config.mapProvider).imports).replace(/\{\{NATIVE_MAP_HOST\}\}/g, () => readFileSync2(join3(TEMPLATES_DIR, androidMapHost(config.mapProvider).template), "utf8")).replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
940
999
  writeFileSync(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
941
1000
  writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftAppLinks.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftAppLinks.kt.template"), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
942
1001
  writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftCrashReporter.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftCrashReporter.kt.template"), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_ANDROID_VERSION));
@@ -979,11 +1038,11 @@ async function init(options) {
979
1038
  mkdirSync2(nativeDir, { recursive: true });
980
1039
  writeFileSync(join3(nativeDir, "CraftNative.kt"), craftNative);
981
1040
  const serviceTemplate = readFileSync2(join3(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
982
- writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
1041
+ writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
983
1042
  const healthTemplate = readFileSync2(join3(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
984
1043
  writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
985
1044
  const manifestTemplate = readFileSync2(join3(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
986
- const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") || config.enableLocalNetwork ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{NATIVE_NOTIFICATION_RECEIVER\}\}/g, config.renderer === "native" ? renderNativeShellManifest(config) : "").replace(/\{\{CRASH_REPORTER_PROVIDER\}\}/g, ` <provider android:name=".CraftCrashReporterInit" android:authorities="\${applicationId}.craft-crash" android:exported="false" android:initOrder="1000" />`).replace(/\{\{GOOGLE_MAPS_API_KEY\}\}/g, config.mapsApiKey ? ` <meta-data android:name="com.google.android.geo.API_KEY" android:value="${escapeXml(config.mapsApiKey)}" />` : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
1045
+ const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") || config.enableLocalNetwork ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{NATIVE_NOTIFICATION_RECEIVER\}\}/g, config.renderer === "native" ? renderNativeShellManifest(config) : "").replace(/\{\{CRASH_REPORTER_PROVIDER\}\}/g, ` <provider android:name=".CraftCrashReporterInit" android:authorities="\${applicationId}.craft-crash" android:exported="false" android:initOrder="1000" />`).replace(/\{\{GOOGLE_MAPS_API_KEY\}\}/g, config.mapProvider !== "maplibre" && config.mapsApiKey ? ` <meta-data android:name="com.google.android.geo.API_KEY" android:value="${escapeXml(config.mapsApiKey)}" />` : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
987
1046
  <package android:name="com.google.android.apps.healthdata" />
988
1047
  </queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
989
1048
  <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
@@ -996,8 +1055,8 @@ async function init(options) {
996
1055
  const projectGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
997
1056
  writeFileSync(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
998
1057
  const appGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
999
- const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{REFRESH_LAYOUT_DEPENDENCY\}\}/g, config.renderer === "native" ? ' implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")' : "").replace(/\{\{GOOGLE_MAPS_DEPENDENCY\}\}/g, config.renderer === "native" ? ` // Native STX Map overlays and trail routes
1000
- implementation("com.google.android.gms:play-services-maps:18.2.0")` : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
1058
+ const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{REFRESH_LAYOUT_DEPENDENCY\}\}/g, config.renderer === "native" ? ' implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")' : "").replace(/\{\{NATIVE_MAP_DEPENDENCY\}\}/g, config.renderer === "native" ? ` // Native STX Map overlays and trail routes
1059
+ implementation("${androidMapHost(config.mapProvider).dependency}")` : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
1001
1060
  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "").replace(/\{\{JAVASCRIPT_ENGINE_DEPENDENCY\}\}/g, config.renderer === "native" ? ` implementation("androidx.javascriptengine:javascriptengine:1.1.0")
1002
1061
  // Native background refresh (WorkManager).
1003
1062
  implementation("androidx.work:work-runtime-ktx:2.9.1")` : "");
@@ -1036,6 +1095,8 @@ zipStorePath=wrapper/dists
1036
1095
  <color name="primary_dark">#16162b</color>
1037
1096
  <color name="accent">#4ade80</color>
1038
1097
  <color name="background">${config.backgroundColor}</color>
1098
+ <!-- Transparent is NotificationCompat's COLOR_DEFAULT: the system picks. -->
1099
+ <color name="craft_notification_color">${config.notificationColor ?? "#00000000"}</color>
1039
1100
  </resources>
1040
1101
  `;
1041
1102
  writeFileSync(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
@@ -1064,6 +1125,21 @@ zipStorePath=wrapper/dists
1064
1125
  } else {
1065
1126
  writeFileSync(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
1066
1127
  }
1128
+ for (const extension of [".png", ".xml"])
1129
+ rmSync2(join3(output, `app/src/main/res/drawable/craft_notification_icon${extension}`), { force: true });
1130
+ if (config.notificationIconPath) {
1131
+ cpSync(config.notificationIconPath, join3(output, `app/src/main/res/drawable/craft_notification_icon${notificationIconExtension}`));
1132
+ } else {
1133
+ writeFileSync(join3(output, "app/src/main/res/drawable/craft_notification_icon.xml"), `<?xml version="1.0" encoding="utf-8"?>
1134
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
1135
+ android:width="24dp"
1136
+ android:height="24dp"
1137
+ android:viewportWidth="108"
1138
+ android:viewportHeight="108">
1139
+ <path android:fillColor="#FFFFFFFF" android:pathData="M18,78L42,42l12,18l12,-12l24,30z" />
1140
+ </vector>
1141
+ `);
1142
+ }
1067
1143
  const themesXml = `<?xml version="1.0" encoding="utf-8"?>
1068
1144
  <resources>
1069
1145
  <style name="Theme.CraftApp" parent="Theme.Material3.DayNight.NoActionBar">
@@ -1286,6 +1362,7 @@ export {
1286
1362
  renderAndroidPermissions,
1287
1363
  renderAndroidDeepLinks,
1288
1364
  open,
1365
+ nativeMapProviders,
1289
1366
  installRuntime,
1290
1367
  init,
1291
1368
  healthConnectPermissions,
@@ -0,0 +1,7 @@
1
+ /** Build-time native map selection. Both providers are native Android views. */
2
+ export type AndroidMapProvider = 'google' | 'maplibre';
3
+ export declare function androidMapHost(provider?: AndroidMapProvider): {
4
+ template: string;
5
+ imports: string;
6
+ dependency: string;
7
+ };
@@ -3475,7 +3475,8 @@ class CraftBridge(
3475
3475
  val notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
3476
3476
 
3477
3477
  val builder = androidx.core.app.NotificationCompat.Builder(activity, notificationChannelId)
3478
- .setSmallIcon(android.R.drawable.ic_dialog_info)
3478
+ .setSmallIcon(R.drawable.craft_notification_icon)
3479
+ .setColor(ContextCompat.getColor(activity, R.color.craft_notification_color))
3479
3480
  .setContentTitle(title)
3480
3481
  .setContentText(body)
3481
3482
  .setPriority(androidx.core.app.NotificationCompat.PRIORITY_DEFAULT)