@glassly/crust 0.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +41 -0
  2. package/android/build.gradle +146 -0
  3. package/android/src/internal/AndroidManifest.xml +9 -0
  4. package/android/src/internal/java/com/glassly/crust/receivers/CaptionsTesterIncidentReceiver.kt +46 -0
  5. package/android/src/main/AndroidManifest.xml +33 -0
  6. package/android/src/main/java/com/glassly/crust/CrustModule.kt +1074 -0
  7. package/android/src/main/java/com/glassly/crust/CrustView.kt +30 -0
  8. package/android/src/main/java/com/glassly/crust/heading/HeadingManager.kt +150 -0
  9. package/android/src/main/java/com/glassly/crust/jsc/JSCDispatcher.kt +189 -0
  10. package/android/src/main/java/com/glassly/crust/jsc/JSCPolyfillBridge.kt +287 -0
  11. package/android/src/main/java/com/glassly/crust/jsc/JSCRuntime.kt +593 -0
  12. package/android/src/main/java/com/glassly/crust/navigation/NavigationManager.kt +1466 -0
  13. package/android/src/main/java/com/glassly/crust/services/NotificationListener.kt +502 -0
  14. package/android/src/main/java/com/glassly/crust/services/NotificationProcessBridge.kt +222 -0
  15. package/android/src/main/java/com/glassly/crust/utils/ImageProcessor.java +452 -0
  16. package/android/src/main/java/com/glassly/crust/utils/VideoStabilizer.kt +556 -0
  17. package/android/src/main/res/values/strings.xml +3 -0
  18. package/app.plugin.js +3 -0
  19. package/build/Crust.types.d.ts +153 -0
  20. package/build/Crust.types.d.ts.map +1 -0
  21. package/build/Crust.types.js +2 -0
  22. package/build/Crust.types.js.map +1 -0
  23. package/build/CrustModule.d.ts +186 -0
  24. package/build/CrustModule.d.ts.map +1 -0
  25. package/build/CrustModule.js +4 -0
  26. package/build/CrustModule.js.map +1 -0
  27. package/build/CrustModule.web.d.ts +39 -0
  28. package/build/CrustModule.web.d.ts.map +1 -0
  29. package/build/CrustModule.web.js +69 -0
  30. package/build/CrustModule.web.js.map +1 -0
  31. package/build/CrustView.d.ts +4 -0
  32. package/build/CrustView.d.ts.map +1 -0
  33. package/build/CrustView.js +8 -0
  34. package/build/CrustView.js.map +1 -0
  35. package/build/CrustView.web.d.ts +3 -0
  36. package/build/CrustView.web.d.ts.map +1 -0
  37. package/build/CrustView.web.js +5 -0
  38. package/build/CrustView.web.js.map +1 -0
  39. package/build/index.d.ts +4 -0
  40. package/build/index.d.ts.map +1 -0
  41. package/build/index.js +6 -0
  42. package/build/index.js.map +1 -0
  43. package/expo-module.config.json +9 -0
  44. package/ios/Crust.podspec +65 -0
  45. package/ios/CrustModule.swift +784 -0
  46. package/ios/CrustView.swift +38 -0
  47. package/ios/Resources/startup.js +804 -0
  48. package/ios/Source/JSCDispatcher.swift +226 -0
  49. package/ios/Source/JSCPolyfillBridge.swift +378 -0
  50. package/ios/Source/JSCRuntime.swift +674 -0
  51. package/ios/Source/utils/ImageProcessor.swift +392 -0
  52. package/ios/Source/utils/SystemGestures.swift +53 -0
  53. package/ios/Source/utils/VideoStabilizer.swift +374 -0
  54. package/ios/heading/HeadingManager.swift +74 -0
  55. package/ios/navigation/NavPayloads.swift +62 -0
  56. package/ios/navigation/NavigationManager.swift +751 -0
  57. package/package.json +69 -0
  58. package/plugin/build/index.d.ts +21 -0
  59. package/plugin/build/index.js +25 -0
  60. package/plugin/build/withAndroid.d.ts +2 -0
  61. package/plugin/build/withAndroid.js +133 -0
  62. package/src/Crust.types.ts +162 -0
  63. package/src/CrustModule.ts +194 -0
  64. package/src/CrustModule.web.ts +72 -0
  65. package/src/CrustView.tsx +10 -0
  66. package/src/CrustView.web.tsx +9 -0
  67. package/src/index.ts +5 -0
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@glassly/crust",
3
+ "version": "0.1.0-dev.1",
4
+ "description": "Glassly Native Module",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "app.plugin": "app.plugin.js",
8
+ "scripts": {
9
+ "build": "expo-module build && expo-module build plugin",
10
+ "clean": "expo-module clean",
11
+ "lint": "expo-module lint",
12
+ "test": "expo-module test",
13
+ "prepare": "EXPO_NONINTERACTIVE=1 expo-module build && EXPO_NONINTERACTIVE=1 expo-module build plugin",
14
+ "prepublishOnly": "expo-module prepublishOnly",
15
+ "expo-module": "expo-module",
16
+ "open:ios": "xed example/ios",
17
+ "open:android": "open -a \"Android Studio\" example/android",
18
+ "build:plugin": "expo-module build plugin"
19
+ },
20
+ "keywords": [
21
+ "react-native",
22
+ "expo",
23
+ "crust",
24
+ "Crust"
25
+ ],
26
+ "repository": "https://github.com/fossephate/crust",
27
+ "bugs": {
28
+ "url": "https://github.com/fossephate/crust/issues"
29
+ },
30
+ "author": "fossephate <6175367+fossephate@users.noreply.github.com> (https://github.com/fossephate)",
31
+ "license": "MIT",
32
+ "homepage": "https://github.com/fossephate/crust#readme",
33
+ "dependencies": {
34
+ "@glassly/jspolyfill": "^0.1.0-dev.0"
35
+ },
36
+ "devDependencies": {
37
+ "@expo/config-plugins": ">=8.0.0",
38
+ "expo-module-scripts": "^56.0.3",
39
+ "typescript": "~5.9.2"
40
+ },
41
+ "peerDependencies": {
42
+ "@types/react": "*",
43
+ "expo": "*",
44
+ "react": "*",
45
+ "react-native": "*"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "files": [
51
+ "android",
52
+ "!android/.gradle",
53
+ "!android/.gradle/**",
54
+ "!android/build",
55
+ "!android/build/**",
56
+ "ios",
57
+ "!ios/build",
58
+ "!ios/build/**",
59
+ "!ios/Pods",
60
+ "!ios/Pods/**",
61
+ "app.plugin.js",
62
+ "build",
63
+ "plugin/build",
64
+ "expo-module.config.json",
65
+ "src",
66
+ "!src/__tests__",
67
+ "README.md"
68
+ ]
69
+ }
@@ -0,0 +1,21 @@
1
+ import { type ConfigPlugin } from "expo/config-plugins";
2
+ /**
3
+ * @glassly/crust config plugin — the module's own Android build contract, so
4
+ * every host that embeds crust (the Glassly app, the example OEM app, real OEM
5
+ * hosts) inherits it by listing "@glassly/crust" in `app.json` plugins instead
6
+ * of hand-mirroring gradle edits:
7
+ *
8
+ * - the authenticated Mapbox Downloads Maven repo (crust bundles the Mapbox
9
+ * Navigation SDK, whose artifacts exist only in Mapbox's private registry;
10
+ * the MAPBOX_DOWNLOADS_TOKEN env / gradle property authenticates it at
11
+ * dependency-resolution time — build-time only, never in the APK)
12
+ * - a global protobuf-javalite exclusion (the bluetooth-sdk/crust native stack
13
+ * needs protobuf-java; Mapbox drags protobuf-javalite transitively; shipping
14
+ * both fails the release build with duplicate classes)
15
+ * - core-library desugaring (crust's AAR metadata requires it of embedding
16
+ * apps — the Nav SDK uses newer core libs)
17
+ * - a generated MainApplication process guard so the notification-listener
18
+ * process never initializes React Native
19
+ */
20
+ declare const withCrust: ConfigPlugin;
21
+ export default withCrust;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const withAndroid_1 = require("./withAndroid");
4
+ /**
5
+ * @glassly/crust config plugin — the module's own Android build contract, so
6
+ * every host that embeds crust (the Glassly app, the example OEM app, real OEM
7
+ * hosts) inherits it by listing "@glassly/crust" in `app.json` plugins instead
8
+ * of hand-mirroring gradle edits:
9
+ *
10
+ * - the authenticated Mapbox Downloads Maven repo (crust bundles the Mapbox
11
+ * Navigation SDK, whose artifacts exist only in Mapbox's private registry;
12
+ * the MAPBOX_DOWNLOADS_TOKEN env / gradle property authenticates it at
13
+ * dependency-resolution time — build-time only, never in the APK)
14
+ * - a global protobuf-javalite exclusion (the bluetooth-sdk/crust native stack
15
+ * needs protobuf-java; Mapbox drags protobuf-javalite transitively; shipping
16
+ * both fails the release build with duplicate classes)
17
+ * - core-library desugaring (crust's AAR metadata requires it of embedding
18
+ * apps — the Nav SDK uses newer core libs)
19
+ * - a generated MainApplication process guard so the notification-listener
20
+ * process never initializes React Native
21
+ */
22
+ const withCrust = (config) => {
23
+ return (0, withAndroid_1.withCrustAndroidBuildContract)(config);
24
+ };
25
+ exports.default = withCrust;
@@ -0,0 +1,2 @@
1
+ import { type ConfigPlugin } from "expo/config-plugins";
2
+ export declare const withCrustAndroidBuildContract: ConfigPlugin;
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withCrustAndroidBuildContract = void 0;
4
+ const config_plugins_1 = require("expo/config-plugins");
5
+ // Marker comments double as idempotency checks: each injection looks for its
6
+ // OWN unique marker (never a shared substring — a marker one block contains
7
+ // can silently mask another block's check).
8
+ const MAPBOX_REPO_MARKER = "mapbox: navigation sdk downloads repo (injected by @glassly/crust)";
9
+ const MAPBOX_REPO = [
10
+ " maven {",
11
+ ` // ${MAPBOX_REPO_MARKER}`,
12
+ " url 'https://api.mapbox.com/downloads/v2/releases/maven'",
13
+ " authentication { basic(BasicAuthentication) }",
14
+ " credentials {",
15
+ " username = 'mapbox'",
16
+ " password = System.getenv('MAPBOX_DOWNLOADS_TOKEN') ?: (findProperty('MAPBOX_DOWNLOADS_TOKEN') ?: '')",
17
+ " }",
18
+ " }",
19
+ ].join("\n");
20
+ const PROTOBUF_EXCLUDE = "exclude group: 'com.google.protobuf', module: 'protobuf-javalite'";
21
+ const NOTIFICATION_PROCESS_GUARD = "crust: skip React Native in notification process";
22
+ const NOTIFICATION_CONFIG_GUARD = "crust: skip Expo lifecycle in notification process";
23
+ function withCrustProjectGradle(config) {
24
+ return (0, config_plugins_1.withProjectBuildGradle)(config, (cfg) => {
25
+ let gradle = cfg.modResults.contents;
26
+ // Mapbox Downloads repo, inserted at the top of allprojects.repositories.
27
+ if (!gradle.includes(MAPBOX_REPO_MARKER)) {
28
+ const reposMatch = gradle.match(/allprojects\s*\{[\s\S]*?repositories\s*\{/);
29
+ if (reposMatch) {
30
+ const idx = (reposMatch.index ?? 0) + reposMatch[0].length;
31
+ gradle = gradle.slice(0, idx) + "\n" + MAPBOX_REPO + gradle.slice(idx);
32
+ }
33
+ else {
34
+ gradle += `\nallprojects {\n repositories {\n${MAPBOX_REPO}\n }\n}\n`;
35
+ }
36
+ }
37
+ // protobuf-javalite exclusion, appended as its own allprojects block so
38
+ // placement never depends on the shape of existing gradle content (other
39
+ // plugins add maven{} entries that break brace-counting regexes).
40
+ if (!gradle.includes(PROTOBUF_EXCLUDE)) {
41
+ gradle += `
42
+ // Exclude protobuf-javalite in every project to avoid duplicate classes with
43
+ // protobuf-java (injected by @glassly/crust).
44
+ allprojects {
45
+ configurations.all {
46
+ ${PROTOBUF_EXCLUDE}
47
+ }
48
+ }
49
+ `;
50
+ }
51
+ cfg.modResults.contents = gradle;
52
+ return cfg;
53
+ });
54
+ }
55
+ function withCrustAppGradle(config) {
56
+ return (0, config_plugins_1.withAppBuildGradle)(config, (cfg) => {
57
+ let gradle = cfg.modResults.contents;
58
+ if (!gradle.includes("coreLibraryDesugaringEnabled")) {
59
+ gradle = gradle.replace(/(namespace\s+['"][^'"]+['"])/, `$1
60
+ compileOptions {
61
+ coreLibraryDesugaringEnabled true
62
+ sourceCompatibility JavaVersion.VERSION_17
63
+ targetCompatibility JavaVersion.VERSION_17
64
+ }`);
65
+ }
66
+ if (!gradle.includes("coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs")) {
67
+ gradle = gradle.replace(/(implementation\("com\.facebook\.react:react-android"\))/, `$1
68
+
69
+ // Required by :glassly-crust (Mapbox Navigation SDK uses newer core libs).
70
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'`);
71
+ }
72
+ cfg.modResults.contents = gradle;
73
+ return cfg;
74
+ });
75
+ }
76
+ function withNotificationProcessGuard(config) {
77
+ return (0, config_plugins_1.withMainApplication)(config, (cfg) => {
78
+ let source = cfg.modResults.contents;
79
+ const isJava = cfg.modResults.language === "java";
80
+ if (!source.includes(NOTIFICATION_PROCESS_GUARD)) {
81
+ const pattern = isJava
82
+ ? /public void onCreate\(\)\s*\{\s*super\.onCreate\(\);/
83
+ : /override fun onCreate\(\)\s*\{\s*super\.onCreate\(\)/;
84
+ const replacement = isJava
85
+ ? `public void onCreate() {
86
+ super.onCreate();
87
+ // ${NOTIFICATION_PROCESS_GUARD}
88
+ if (com.glassly.crust.services.NotificationProcess.isCurrent(this)) {
89
+ return;
90
+ }`
91
+ : `override fun onCreate() {
92
+ super.onCreate()
93
+ // ${NOTIFICATION_PROCESS_GUARD}
94
+ if (com.glassly.crust.services.NotificationProcess.isCurrent(this)) {
95
+ return
96
+ }`;
97
+ if (!pattern.test(source)) {
98
+ throw new Error("@glassly/crust could not add the notification-process guard to MainApplication");
99
+ }
100
+ source = source.replace(pattern, replacement);
101
+ }
102
+ if (!source.includes(NOTIFICATION_CONFIG_GUARD)) {
103
+ const pattern = isJava
104
+ ? /public void onConfigurationChanged\(Configuration newConfig\)\s*\{\s*super\.onConfigurationChanged\(newConfig\);/
105
+ : /override fun onConfigurationChanged\(newConfig: Configuration\)\s*\{\s*super\.onConfigurationChanged\(newConfig\)/;
106
+ const replacement = isJava
107
+ ? `public void onConfigurationChanged(Configuration newConfig) {
108
+ super.onConfigurationChanged(newConfig);
109
+ // ${NOTIFICATION_CONFIG_GUARD}
110
+ if (com.glassly.crust.services.NotificationProcess.isCurrent(this)) {
111
+ return;
112
+ }`
113
+ : `override fun onConfigurationChanged(newConfig: Configuration) {
114
+ super.onConfigurationChanged(newConfig)
115
+ // ${NOTIFICATION_CONFIG_GUARD}
116
+ if (com.glassly.crust.services.NotificationProcess.isCurrent(this)) {
117
+ return
118
+ }`;
119
+ // Some custom hosts do not override onConfigurationChanged. In that case
120
+ // there is no Expo lifecycle callback to guard.
121
+ if (pattern.test(source))
122
+ source = source.replace(pattern, replacement);
123
+ }
124
+ cfg.modResults.contents = source;
125
+ return cfg;
126
+ });
127
+ }
128
+ const withCrustAndroidBuildContract = (config) => {
129
+ config = withCrustProjectGradle(config);
130
+ config = withCrustAppGradle(config);
131
+ return withNotificationProcessGuard(config);
132
+ };
133
+ exports.withCrustAndroidBuildContract = withCrustAndroidBuildContract;
@@ -0,0 +1,162 @@
1
+ import type {StyleProp, ViewStyle} from "react-native"
2
+
3
+ export type OnLoadEventPayload = {
4
+ url: string
5
+ }
6
+
7
+ export type CrustModuleEvents = {
8
+ onChange: (params: ChangeEventPayload) => void
9
+ onNavManeuver: (params: NavManeuverPayload) => void
10
+ onNavRerouting: (params: Record<string, never>) => void
11
+ onNavArrived: (params: Record<string, never>) => void
12
+ onNavError: (params: NavErrorPayload) => void
13
+ onNavLocation: (params: NavLocationPayload) => void
14
+ onNavRoute: (params: NavRoutePayload) => void
15
+ onNavOffRoute: (params: NavOffRoutePayload) => void
16
+ onHeading: (params: HeadingPayload) => void
17
+ }
18
+
19
+ export type NavOffRoutePayload = {
20
+ /** Approximate perpendicular distance in meters from the route. */
21
+ offRouteDistanceMeters: number
22
+ }
23
+
24
+ export type HeadingPayload = {
25
+ /** Compass heading in degrees, 0 = north, 90 = east. */
26
+ degrees: number
27
+ }
28
+
29
+ export type NavRoutePayload = {
30
+ points: Array<{lat: number; lng: number}>
31
+ /**
32
+ * Ordered step list along the route, when supplied by the host. Each
33
+ * step is one navigable segment — typically one straight stretch of
34
+ * road ending in a maneuver. Optional for back-compat with older
35
+ * hosts that only ship the polyline. The SDK's pivot module consumes
36
+ * this to enrich pivots with `fromRoad` / `toRoad` metadata.
37
+ */
38
+ steps?: NavRouteStepPayload[]
39
+ }
40
+
41
+ export type NavRouteStepPayload = {
42
+ /** Coordinate where this step begins (== end of the previous step). */
43
+ lat: number
44
+ lng: number
45
+ /**
46
+ * Index into `NavRoutePayload.points[]` where this step starts.
47
+ * Lets consumers correlate step boundaries with polyline geometry.
48
+ */
49
+ routeIndex: number
50
+ /** Name of the road traversed during this step. Null when the engine has no name. */
51
+ road?: string | null
52
+ /**
53
+ * Categorical maneuver performed at the END of this step (i.e. how
54
+ * the user exits this step). Same vocabulary as `NavManeuverPayload.maneuverType`.
55
+ */
56
+ maneuver: string
57
+ /** Length of this step in meters. */
58
+ distanceMeters: number
59
+ }
60
+
61
+ export type NavLocationPayload = {
62
+ lat: number
63
+ lng: number
64
+ /** Horizontal accuracy in meters, if reported by the platform. */
65
+ accuracy: number | null
66
+ /** Unix ms timestamp of the fix. */
67
+ timestamp: number
68
+ phone_notification: (event: PhoneNotificationEvent) => void
69
+ phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void
70
+ captions_tester_incident: (event: CaptionsTesterIncidentEvent) => void
71
+ }
72
+
73
+ export type ChangeEventPayload = {
74
+ value: string
75
+ }
76
+
77
+ export type NavManeuverPayload = {
78
+ /**
79
+ * Categorical type of the upcoming maneuver. One of: STRAIGHT,
80
+ * SLIGHT_LEFT, SLIGHT_RIGHT, TURN_LEFT, TURN_RIGHT, SHARP_LEFT,
81
+ * SHARP_RIGHT, U_TURN, ARRIVE.
82
+ */
83
+ maneuverType: string
84
+ /** Distance in meters from the user's current position to that maneuver. -1 if unknown. */
85
+ distanceMeters: number
86
+ /** Road the user is currently on, per the Nav SDK. Null if unavailable. */
87
+ fromRoad?: string | null
88
+ /**
89
+ * Legacy "next road" field — historically populated from the same
90
+ * source as `fromRoad` (the current step's road, NOT the road
91
+ * after the upcoming turn). Kept for back-compat. New consumers
92
+ * should read `nextStepRoad` instead.
93
+ */
94
+ toRoad?: string | null
95
+ /**
96
+ * Road the user will be on AFTER the upcoming maneuver, sourced
97
+ * from the Nav SDK's `remainingSteps[0]`. This is the value to
98
+ * show as the "next street" headline on the maneuver card. Null
99
+ * when the SDK hasn't surfaced a remaining step yet (final leg,
100
+ * pre-first-NavInfo, etc.).
101
+ */
102
+ nextStepRoad?: string | null
103
+ /** Total remaining distance to final destination, meters. -1 if unknown. */
104
+ distanceToDestinationMeters?: number
105
+ /** Total remaining travel time, seconds. -1 if unknown. */
106
+ timeToDestinationSeconds?: number
107
+ /** Current speed in m/s. Null if unavailable. */
108
+ currentSpeedMps?: number | null
109
+ /** Speed limit on the current road segment in m/s. Null if unknown / not regulated. */
110
+ speedLimitMps?: number | null
111
+ /** Bearing along the route at the user's current position, 0–360. Null if unknown. */
112
+ routeHeadingDeg?: number | null
113
+ }
114
+
115
+ export type NavErrorPayload = {
116
+ message: string
117
+ }
118
+
119
+ export type InstalledApp = {
120
+ packageName: string
121
+ appName: string
122
+ isBlocked: boolean
123
+ icon: string | null
124
+ }
125
+
126
+ export type PhoneNotificationEvent = {
127
+ notificationId: string
128
+ app: string
129
+ title: string
130
+ content: string
131
+ /**
132
+ * Android notification priority: PRIORITY_MIN (-2) through PRIORITY_MAX (2).
133
+ * Derived from the channel importance when the ranking is available, since
134
+ * Notification.priority reads 0 for channel-based notifications.
135
+ */
136
+ priority: number
137
+ timestamp: number
138
+ packageName: string
139
+ }
140
+
141
+ export type PhoneNotificationDismissedEvent = {
142
+ notificationKey: string
143
+ packageName: string
144
+ notificationId: string
145
+ }
146
+
147
+ export type CaptionsTesterIncidentEvent = {
148
+ action?: string
149
+ timestamp?: number
150
+ failure_code?: string
151
+ failure_message?: string
152
+ test_run_id?: string
153
+ scenario_name?: string
154
+ source?: string
155
+ [key: string]: unknown
156
+ }
157
+
158
+ export type CrustViewProps = {
159
+ url: string
160
+ onLoad: (event: {nativeEvent: OnLoadEventPayload}) => void
161
+ style?: StyleProp<ViewStyle>
162
+ }
@@ -0,0 +1,194 @@
1
+ import {NativeModule, requireNativeModule} from "expo"
2
+
3
+ import type {CrustModuleEvents, InstalledApp} from "./Crust.types"
4
+
5
+ declare class CrustModule extends NativeModule<CrustModuleEvents> {
6
+ PI: number
7
+ hello(): string
8
+ setValueAsync(value: string): Promise<void>
9
+ nativeHttpRequest(
10
+ method: string,
11
+ url: string,
12
+ headers: Record<string, string>,
13
+ body?: string | null,
14
+ ): Promise<{status: number; statusText: string; headers: Record<string, string>; body: string}>
15
+ showAVRoutePicker(tintColor?: string | null): void
16
+
17
+ /**
18
+ * iOS: configure `preferredScreenEdgesDeferringSystemGestures`. When an
19
+ * edge is deferred, the first swipe across that edge is consumed by the
20
+ * app and the system gesture (Control Center, Notification Center, Home
21
+ * indicator) only fires on a second swipe — i.e. a two-swipe-to-exit UX.
22
+ *
23
+ * Pass `[]` to restore default behavior. Android: no-op (Android has no
24
+ * per-app equivalent; system gestures are configured at the OS level).
25
+ */
26
+ setDeferredSystemGestures(edges: Array<"top" | "bottom" | "left" | "right" | "all">): Promise<void>
27
+
28
+ // Glassly Notification Commands
29
+ setNotificationConfig(listenerEnabled: boolean, blocklist: string[]): Promise<void>
30
+ getInstalledApps(): Promise<InstalledApp[]>
31
+ getInstalledAppsForNotifications(): Promise<InstalledApp[]>
32
+ hasNotificationListenerPermission(): Promise<boolean>
33
+ refreshNotificationListener(): Promise<boolean>
34
+ openNotificationListenerSettings(): Promise<boolean>
35
+ isBetaBuild(): Promise<boolean>
36
+ // location services commands
37
+ showLocationServicesDialog(): Promise<boolean>
38
+ isLocationServicesEnabled(): Promise<boolean>
39
+ openLocationSettings(): Promise<boolean>
40
+ openAppSettings(): Promise<boolean>
41
+ openBluetoothSettings(): Promise<boolean>
42
+
43
+ // Image Processing Commands
44
+ processGalleryImage(
45
+ inputPath: string,
46
+ outputPath: string,
47
+ options: {
48
+ lensCorrection?: boolean
49
+ colorCorrection?: boolean
50
+ },
51
+ ): Promise<{
52
+ success: boolean
53
+ outputPath?: string
54
+ processingTimeMs?: number
55
+ error?: string
56
+ }>
57
+
58
+ mergeHdrBrackets(
59
+ underPath: string,
60
+ normalPath: string,
61
+ overPath: string,
62
+ outputPath: string,
63
+ ): Promise<{
64
+ success: boolean
65
+ outputPath?: string
66
+ processingTimeMs?: number
67
+ error?: string
68
+ }>
69
+
70
+ stabilizeVideo(
71
+ inputPath: string,
72
+ imuPath: string,
73
+ outputPath: string,
74
+ ): Promise<{
75
+ success: boolean
76
+ outputPath?: string
77
+ processingTimeMs?: number
78
+ error?: string
79
+ }>
80
+
81
+ // Media Library Commands
82
+ saveToGalleryWithDate(
83
+ filePath: string,
84
+ captureTimeMillis?: number,
85
+ displayName?: string,
86
+ ): Promise<{
87
+ success: boolean
88
+ uri?: string
89
+ identifier?: string
90
+ existing?: boolean
91
+ error?: string
92
+ }>
93
+
94
+ // Navigation (both platforms via Mapbox; the dev toggles below are Android-only)
95
+ /**
96
+ * Override the Mapbox access token the native Navigation SDK
97
+ * authenticates with. A non-empty token wins over the one baked at build
98
+ * time (AndroidManifest `com.mapbox.token` / Info.plist `MBXAccessToken`);
99
+ * "" clears the override and restores the baked token. Call before
100
+ * `startNavigation` — the engine pushes the user's Settings > Keys token
101
+ * here per trip start.
102
+ */
103
+ setMapboxAccessToken(token: string): Promise<{ok: boolean; error?: string}>
104
+ startNavigation(
105
+ lat: number,
106
+ lng: number,
107
+ options?: {
108
+ simulate?: boolean
109
+ speedMultiplier?: number
110
+ /** Optional multi-stop list. When present takes precedence over lat/lng. Last entry is the final destination. */
111
+ stops?: Array<{lat: number; lng: number}>
112
+ /** "walking" | "driving" | "cycling" | "two_wheeler". Defaults driving. */
113
+ mode?: string
114
+ avoid?: {highways?: boolean; tolls?: boolean; ferries?: boolean}
115
+ /** Force a reroute when the user is more than N meters past a pivot they didn't take. */
116
+ missedTurnRerouteMeters?: number
117
+ },
118
+ ): Promise<{ok: boolean; error?: string}>
119
+ stopNavigation(): Promise<{ok: boolean; error?: string}>
120
+
121
+ /**
122
+ * Kept for API parity with the Google-era contract. Mapbox has no
123
+ * Terms & Conditions dialog, so no dialog is shown; resolves with
124
+ * `{accepted: true}` once permission handling completes. Idempotent —
125
+ * still safe (and intended) to call eagerly.
126
+ */
127
+ requestNavigationPermission(): Promise<{ok: boolean; accepted: boolean; error?: string}>
128
+
129
+ /**
130
+ * Dev-only: push the simulated user position off-route so the Nav SDK
131
+ * reroutes. Only meaningful in simulate mode. Both platforms;
132
+ * `offsetMeters` tunes the jump on Android and is ignored on iOS.
133
+ */
134
+ simulateDeviation(offsetMeters?: number): Promise<{ok: boolean; error?: string}>
135
+
136
+ /**
137
+ * Dev toggle. When enabled, the native NavigationManager shifts every
138
+ * reported location ~8m perpendicular to the route bearing, simulating
139
+ * a pedestrian walking on the wrong sidewalk. Only meaningful in
140
+ * simulate mode; lets us verify the SDK's along-path pivot trigger
141
+ * fires even when the user never crosses the 7m pivot point radius.
142
+ * Android-only today; iOS is a no-op stub.
143
+ */
144
+ setWrongSidewalkOffset(enabled: boolean): Promise<{ok: boolean; error?: string}>
145
+
146
+ /**
147
+ * Dev toggle. When enabled, the native NavigationManager takes over
148
+ * from the Mapbox simulator and walks the user along a modified
149
+ * polyline that omits crossing micro-steps — reproducing the
150
+ * wrong-sidewalk-then-missed-the-turn scenario. Android-only today;
151
+ * iOS is a no-op stub.
152
+ */
153
+ setSkipCrossings(enabled: boolean): Promise<{ok: boolean; error?: string}>
154
+
155
+ // Heading / compass (both platforms)
156
+ startHeading(): Promise<{ok: boolean; error?: string}>
157
+ stopHeading(): Promise<{ok: boolean; error?: string}>
158
+
159
+ // GlasslyJS Runtime — per-miniapp JSContext lifecycle.
160
+ /**
161
+ * Spawn a per-miniapp JS context. Re-spawn is allowed: a live context
162
+ * with the same packageName is killed first. Returns true if the
163
+ * polyfill bundle + miniapp source evaluated without throwing.
164
+ *
165
+ * The polyfill bundle is `mobile/modules/glasslyjs-runtime/dist/startup.js`
166
+ * (shipped inside the host binary). It installs window-style globals
167
+ * (console, timers, fetch, localStorage, crypto) atop the JSC runtime.
168
+ */
169
+ jsSpawn(packageName: string, polyfillBundle: string, miniappJs: string): Promise<boolean>
170
+ jsEvaluate(packageName: string, source: string): Promise<unknown>
171
+ jsKill(packageName: string): Promise<void>
172
+ /**
173
+ * Push a `{kind: "event"|"response", …}` envelope into the named
174
+ * context's globalThis.__deliver. Returns when the underlying
175
+ * evaluateScript completes (the JS handler runs synchronously on the
176
+ * context's queue).
177
+ */
178
+ jsDispatchToJs(packageName: string, envelope: Record<string, unknown>): Promise<void>
179
+ jsSetManifest(packageName: string, permissions: string[]): Promise<void>
180
+ /** Diagnostic — returns the packageNames of every live JSContext. */
181
+ jsAlivePackages(): string[]
182
+ /** Diagnostic — force a GC cycle on the named context. */
183
+ jsDebugForceGC(packageName: string): Promise<boolean>
184
+ /**
185
+ * Read the bundled GlasslyJS polyfill (startup.js) shipped inside the
186
+ * host binary. Synchronous — host RN code calls this once on app boot
187
+ * and caches the string, then passes it to every jsSpawn so
188
+ * every JSContext starts with the same polyfill ABI.
189
+ */
190
+ jsLoadPolyfillBundle(): string
191
+ }
192
+
193
+ // This call loads the native module object from the JSI.
194
+ export default requireNativeModule<CrustModule>("Crust")
@@ -0,0 +1,72 @@
1
+ import {registerWebModule, NativeModule} from "expo"
2
+
3
+ import type {CrustModuleEvents} from "./Crust.types"
4
+
5
+ class CrustModule extends NativeModule<CrustModuleEvents> {
6
+ PI = Math.PI
7
+ async setValueAsync(value: string): Promise<void> {
8
+ this.emit("onChange", {value})
9
+ }
10
+ async nativeHttpRequest(method: string, url: string, headers: Record<string, string>, body?: string | null) {
11
+ const response = await fetch(url, {method, headers, body})
12
+ return {
13
+ status: response.status,
14
+ statusText: response.statusText,
15
+ headers: Object.fromEntries(response.headers.entries()),
16
+ body: await response.text(),
17
+ }
18
+ }
19
+ hello() {
20
+ return "Hello world! 👋"
21
+ }
22
+ showAVRoutePicker(_tintColor?: string | null) {}
23
+ async setDeferredSystemGestures(_edges: string[]): Promise<void> {}
24
+ async setNotificationConfig(_listenerEnabled: boolean, _blocklist: string[]): Promise<void> {}
25
+ async getInstalledApps() {
26
+ return []
27
+ }
28
+ async getInstalledAppsForNotifications() {
29
+ return []
30
+ }
31
+ async hasNotificationListenerPermission() {
32
+ return false
33
+ }
34
+ async refreshNotificationListener() {
35
+ return false
36
+ }
37
+ async openNotificationListenerSettings() {
38
+ return false
39
+ }
40
+ async isBetaBuild() {
41
+ return false
42
+ }
43
+ async setMapboxAccessToken(_token: string) {
44
+ return {ok: false, error: "Not supported on web"}
45
+ }
46
+ async jsSpawn(_pkg: string, _polyfill: string, _miniappJs: string) {
47
+ return false
48
+ }
49
+ async jsEvaluate(_pkg: string, _src: string) {
50
+ return null
51
+ }
52
+ async jsKill(_pkg: string) {
53
+ return
54
+ }
55
+ async jsDispatchToJs(_pkg: string, _env: Record<string, unknown>) {
56
+ return
57
+ }
58
+ async jsSetManifest(_pkg: string, _perms: string[]) {
59
+ return
60
+ }
61
+ jsAlivePackages() {
62
+ return []
63
+ }
64
+ async jsDebugForceGC(_pkg: string) {
65
+ return false
66
+ }
67
+ jsLoadPolyfillBundle() {
68
+ return ""
69
+ }
70
+ }
71
+
72
+ export default registerWebModule(CrustModule, "CrustModule")