@mentra/engine 3.2.0-dev.181 → 3.2.0-dev.190

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 (59) hide show
  1. package/README.md +13 -0
  2. package/build/engine.d.ts +7 -0
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/facades/dev.d.ts.map +1 -1
  5. package/build/facades/dev.js +30 -0
  6. package/build/facades/dev.js.map +1 -1
  7. package/build/facades/phoneNotifications.d.ts +9 -0
  8. package/build/facades/phoneNotifications.d.ts.map +1 -1
  9. package/build/facades/phoneNotifications.js +15 -0
  10. package/build/facades/phoneNotifications.js.map +1 -1
  11. package/build/generated/releaseMetadata.js +5 -5
  12. package/build/generated/releaseMetadata.js.map +1 -1
  13. package/build/runtime/bootstrap.d.ts +9 -0
  14. package/build/runtime/bootstrap.d.ts.map +1 -1
  15. package/build/runtime/bootstrap.js.map +1 -1
  16. package/build/services/CloudClientService.d.ts +1 -1
  17. package/build/services/CloudClientService.d.ts.map +1 -1
  18. package/build/services/CloudClientService.js +3 -17
  19. package/build/services/CloudClientService.js.map +1 -1
  20. package/build/services/NativeNotificationPolicy.d.ts +13 -0
  21. package/build/services/NativeNotificationPolicy.d.ts.map +1 -0
  22. package/build/services/NativeNotificationPolicy.js +12 -0
  23. package/build/services/NativeNotificationPolicy.js.map +1 -0
  24. package/build/services/PhoneNotificationsSync.d.ts +13 -0
  25. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  26. package/build/services/PhoneNotificationsSync.js +102 -26
  27. package/build/services/PhoneNotificationsSync.js.map +1 -1
  28. package/build/services/cloudEndpointPolicy.d.ts +19 -0
  29. package/build/services/cloudEndpointPolicy.d.ts.map +1 -0
  30. package/build/services/cloudEndpointPolicy.js +20 -0
  31. package/build/services/cloudEndpointPolicy.js.map +1 -0
  32. package/build/services/otaManifestPolicy.js +1 -1
  33. package/build/services/otaManifestPolicy.js.map +1 -1
  34. package/build/services/otaManifestUrl.d.ts.map +1 -1
  35. package/build/services/otaManifestUrl.js +2 -1
  36. package/build/services/otaManifestUrl.js.map +1 -1
  37. package/build/stores/settings.d.ts.map +1 -1
  38. package/build/stores/settings.js +46 -5
  39. package/build/stores/settings.js.map +1 -1
  40. package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
  41. package/build/types/capabilities/even-realities-g2.js +1 -0
  42. package/build/types/capabilities/even-realities-g2.js.map +1 -1
  43. package/build/types/hardware.d.ts +2 -0
  44. package/build/types/hardware.d.ts.map +1 -1
  45. package/build/types/hardware.js.map +1 -1
  46. package/package.json +7 -7
  47. package/src/facades/dev.ts +32 -0
  48. package/src/facades/phoneNotifications.ts +21 -0
  49. package/src/generated/releaseMetadata.ts +5 -5
  50. package/src/runtime/bootstrap.ts +6 -0
  51. package/src/services/CloudClientService.ts +3 -15
  52. package/src/services/NativeNotificationPolicy.ts +24 -0
  53. package/src/services/PhoneNotificationsSync.ts +115 -31
  54. package/src/services/cloudEndpointPolicy.ts +25 -0
  55. package/src/services/otaManifestPolicy.ts +1 -1
  56. package/src/services/otaManifestUrl.ts +2 -1
  57. package/src/stores/settings.ts +49 -13
  58. package/src/types/capabilities/even-realities-g2.ts +1 -0
  59. package/src/types/hardware.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/engine",
3
- "version": "3.2.0-dev.181",
3
+ "version": "3.2.0-dev.190",
4
4
  "description": "Mentra Engine — miniapp registry, WebView bridge, and miniapp store",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -97,12 +97,12 @@
97
97
  "registry": "https://registry.npmjs.org/"
98
98
  },
99
99
  "dependencies": {
100
- "@mentra/acs-meeting": "3.2.0-dev.181",
101
- "@mentra/bluetooth-sdk": "3.2.0-dev.181",
102
- "@mentra/cloud-client": "3.2.0-dev.181",
103
- "@mentra/cloud-protocol": "3.2.0-dev.181",
104
- "@mentra/crust": "3.2.0-dev.181",
105
- "@mentra/miniapp": "3.2.0-dev.181",
100
+ "@mentra/acs-meeting": "3.2.0-dev.190",
101
+ "@mentra/bluetooth-sdk": "3.2.0-dev.190",
102
+ "@mentra/cloud-client": "3.2.0-dev.190",
103
+ "@mentra/cloud-protocol": "3.2.0-dev.190",
104
+ "@mentra/crust": "3.2.0-dev.190",
105
+ "@mentra/miniapp": "3.2.0-dev.190",
106
106
  "buffer": "^6.0.3",
107
107
  "events": "^3.3.0",
108
108
  "react-native-marked": "8.1.1",
@@ -19,6 +19,7 @@ import {getConfigValues} from "../runtime/bootstrap"
19
19
  import {decideDevLaunchRoute} from "../utils/devMiniappLaunch"
20
20
  import {registerDevApp, DEV_APP_PACKAGE_NAME, type DevAppRecord} from "../services/AppRegistry"
21
21
  import {useAppStatusStore} from "../stores/apps"
22
+ import {scopeCloudUrlOverrides} from "../services/cloudEndpointPolicy"
22
23
 
23
24
  type CloudUrlOverrides = {core?: string; runtime?: string}
24
25
 
@@ -28,6 +29,11 @@ type CloudUrlOverrides = {core?: string; runtime?: string}
28
29
  * host's Metro/env URL resolution); both empty clears the live override.
29
30
  */
30
31
  function applyCloudUrlReconnect(urls: CloudUrlOverrides = currentCloudUrlOverrides()): void {
32
+ const hostResolver = getConfigValues().resolveCloudEndpoints
33
+ if (hostResolver) {
34
+ cloudClientService.reconnect(null)
35
+ return
36
+ }
31
37
  const resolved = resolveCloudUrlReconnectTarget(urls)
32
38
  if (resolved === undefined) {
33
39
  cloudClientService.reconnect(null)
@@ -91,6 +97,32 @@ export const dev = {
91
97
  },
92
98
  /** Override the cloud-v2 URLs and reconnect the live client onto them. */
93
99
  setCloudUrls: (urls: {core?: string; runtime?: string}) => {
100
+ const config = getConfigValues()
101
+ if (config.resolveCloudEndpoints) {
102
+ const scope = config.cloudDebugScope ?? ""
103
+ const scoped = scopeCloudUrlOverrides(
104
+ {
105
+ scope: useSettingsStore.getState().getSetting(SETTINGS.cloud_url_deployment.key),
106
+ ...currentCloudUrlOverrides(),
107
+ },
108
+ scope,
109
+ urls,
110
+ )
111
+ const values: Record<string, unknown> = {
112
+ [SETTINGS.cloud_url_deployment.key]: scope,
113
+ [SETTINGS.cloud_core_url.key]: scoped.core,
114
+ [SETTINGS.cloud_runtime_url.key]: scoped.runtime,
115
+ [SETTINGS.cached_required_version.key]: "",
116
+ }
117
+ void useSettingsStore
118
+ .getState()
119
+ .setManyLocally(values)
120
+ .then((result) => {
121
+ if (result.is_error()) console.error("engine.dev.setCloudUrls: could not save URLs", result.error)
122
+ else applyCloudUrlReconnect()
123
+ })
124
+ return
125
+ }
94
126
  const next = {...currentCloudUrlOverrides(), ...urls}
95
127
  const resolved = resolveCloudUrlReconnectTarget(next)
96
128
  if (resolved === null) {
@@ -10,9 +10,30 @@
10
10
  */
11
11
  import {Platform} from "react-native"
12
12
  import CrustModule from "@mentra/crust"
13
+ import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
14
+ import type {NativeNotificationStatus, NativeNotificationDelivery} from "@mentra/bluetooth-sdk"
15
+ import {
16
+ nativeNotificationCapabilities,
17
+ setPhoneNotificationPresentationActive,
18
+ usesNativeNotificationPresentation,
19
+ presentNativePhoneNotification,
20
+ } from "../services/PhoneNotificationsSync"
13
21
  import {useSettingsStore, SETTINGS} from "../stores/settings"
14
22
 
15
23
  export const phoneNotifications = {
24
+ nativeCapabilities: nativeNotificationCapabilities,
25
+ setPresentationActive: setPhoneNotificationPresentationActive,
26
+ usesNativePresentation: usesNativeNotificationPresentation,
27
+ presentNative: presentNativePhoneNotification,
28
+ nativeStatus: (): Promise<NativeNotificationStatus> => BluetoothSdk.getNativeNotificationStatus(),
29
+ onNativeStatus: (listener: (status: NativeNotificationStatus) => void) => {
30
+ const subscription = BluetoothSdk.addListener("native_notification_status", listener)
31
+ return () => subscription.remove()
32
+ },
33
+ onNativeDelivery: (listener: (event: NativeNotificationDelivery) => void) => {
34
+ const subscription = BluetoothSdk.addListener("native_notification_delivery", listener)
35
+ return () => subscription.remove()
36
+ },
16
37
  /** The phone's installed apps (package, name, icon). (empty on iOS) */
17
38
  installedApps: (): Promise<Array<{packageName: string; appName: string; icon: string | null}>> => {
18
39
  if (Platform.OS !== "android") return Promise.resolve([])
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.2.0",
15
- "releaseIdentity": "3.2.0-dev.181",
16
- "releaseSetId": "mentra-3.2.0-dev.181",
17
- "sourceCommit": "89a4792321ce95bd16aadd721bc3dbc20e338861",
18
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.181.json",
19
- "otaManifestSha256": "d9a100cc81733d02848a86c7617eff154f4cd9d714d1d36407789aeebbf71251"
15
+ "releaseIdentity": "3.2.0-dev.190",
16
+ "releaseSetId": "mentra-3.2.0-dev.190",
17
+ "sourceCommit": "9cec54c1238d6d52a90e2edc84f0b99692b8a81a",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.190.json",
19
+ "otaManifestSha256": "00504171cce268dae2a22d9c4156cb7bac0b8a4397b35a3890ed561c619e5df0"
20
20
  })
@@ -28,6 +28,10 @@ export interface IslandConfigValues {
28
28
  coreUrl?: string | null
29
29
  /** cloud-v2 runtime service base URL. */
30
30
  runtimeUrl?: string | null
31
+ /** Host's live manifest/override resolver, also used on automatic reconnects. */
32
+ resolveCloudEndpoints?: () => {core: string; runtime: string}
33
+ /** Scope for debug settings written through engine.dev. */
34
+ cloudDebugScope?: string
31
35
  /** Open Runtime's live WebSocket/audio session. Defaults to true. */
32
36
  runtimeRealtimeSession?: boolean
33
37
  /** Complete allowlist for bundled/local miniapps; null or omitted allows all. */
@@ -40,6 +44,8 @@ export interface IslandConfigValues {
40
44
  cloudAuthStorageKey?: string
41
45
  /** Deployment-pinned Mentra Live OTA manifest; explicit null disables remote OTA. */
42
46
  otaManifestUrl?: string | null
47
+ /** Official deployments retain the legacy-device and embedded release fallback. */
48
+ allowLegacyOtaFallback?: boolean
43
49
  /** Deployment capability policy. Omitted entries preserve consumer behavior. */
44
50
  features?: Partial<Record<IslandFeatureName, boolean>>
45
51
  /** OEM identifier (Mentra is OEM #0); reserved for OEM auth/telemetry. */
@@ -34,6 +34,7 @@ import {BgTimer} from "../utils/timers"
34
34
  import {logCloudV2TranscriptMetric} from "./CloudTranscriptE2EMetrics"
35
35
  import {LocalMiniappUserIdentity} from "./LocalMiniappUserIdentity"
36
36
  import {nativeHttpResponseBody} from "./NativeHttpResponse"
37
+ import {resolveCloudEndpoints} from "./cloudEndpointPolicy"
37
38
 
38
39
  const LOG_TAG = "cloudClient"
39
40
  type CloudCore = NonNullable<CloudClient["core"]>
@@ -56,11 +57,6 @@ function clearPersistentFailureAlarm(): void {
56
57
 
57
58
  type Lc3FrameSizeBytes = 20 | 40 | 60
58
59
 
59
- // Neutral last-ditch fallbacks (reachable under `adb reverse`) for when the host
60
- // passes no endpoints. The host normally resolves the real URLs (dev override /
61
- // Metro host / env) and hands them in via `engine.configure({config})`.
62
- const FALLBACK_CORE_URL = "http://localhost:3000"
63
- const FALLBACK_RUNTIME_URL = "http://localhost:3001"
64
60
  const LOCAL_AUTH_PORT = 3002
65
61
 
66
62
  let client: CloudClient | null = null
@@ -100,15 +96,7 @@ const statusListeners = new Set<(snapshot: CloudClientStatusSnapshot) => void>()
100
96
  const connectionListeners = new Set<(connected: boolean) => void>()
101
97
 
102
98
  function resolveEndpoints(): {core?: string; runtime: string} {
103
- if (endpointsOverride) return endpointsOverride
104
- const cfg = getConfigValues()
105
- const runtime = cfg.runtimeUrl === null ? "" : cfg.runtimeUrl?.trim() || FALLBACK_RUNTIME_URL
106
- if (!runtime) throw new Error("cloudClient: Runtime endpoint is not configured")
107
- const core = cfg.coreUrl === null ? undefined : cfg.coreUrl?.trim() || FALLBACK_CORE_URL
108
- return {
109
- ...(core ? {core} : {}),
110
- runtime,
111
- }
99
+ return resolveCloudEndpoints(getConfigValues(), endpointsOverride)
112
100
  }
113
101
 
114
102
  function getCoreClient(): CloudCore {
@@ -554,7 +542,7 @@ export const cloudClientService = {
554
542
 
555
543
  /**
556
544
  * Tear down + rebuild. Pass new endpoints to switch URLs; pass `null` to CLEAR
557
- * a prior override and fall back to the boot config (so cleared/default cloud
545
+ * a prior override and resume the host resolver or boot config (so cleared/default cloud
558
546
  * URLs don't keep reconnecting to a stale override); omit to keep the current.
559
547
  */
560
548
  reconnect(endpoints?: {core?: string; runtime: string} | null): void {
@@ -0,0 +1,24 @@
1
+ import type {NativeNotificationConfig} from "@mentra/bluetooth-sdk"
2
+
3
+ export interface NotificationPolicyInput {
4
+ supported: boolean
5
+ selected: boolean
6
+ presentationActive: boolean
7
+ sourceEnabled: boolean
8
+ autoDisplay: boolean
9
+ durationSeconds: number
10
+ doNotDisturb: boolean
11
+ blockedApps: string[]
12
+ }
13
+
14
+ export function nativeNotificationConfig(input: NotificationPolicyInput): NativeNotificationConfig {
15
+ return {
16
+ enabled: input.supported && input.selected && input.presentationActive && input.sourceEnabled,
17
+ autoDisplay: input.autoDisplay,
18
+ durationSeconds: Number.isFinite(input.durationSeconds)
19
+ ? Math.min(30, Math.max(1, Math.round(input.durationSeconds)))
20
+ : 5,
21
+ doNotDisturb: input.doNotDisturb,
22
+ blockedApps: input.blockedApps,
23
+ }
24
+ }
@@ -1,46 +1,130 @@
1
- /**
2
- * Phone-notifications sync — engine-owned. Pushes the notification-forwarding
3
- * operational kill switch + config (`android_notification_listener_enabled`
4
- * and `notifications_blocklist`) to the native
5
- * NotificationListener via `CrustModule.setNotificationConfig`, so
6
- * the blocklist reaches the listener for ANY host — not just the Mentra App,
7
- * where this used to live in MantleManager. The Android service is desired by
8
- * default now that its cold start is isolated from React Native. Native code
9
- * keeps the component discoverable in Android's notification-access Settings,
10
- * but only binds the service after access is granted. The debug setting remains
11
- * a hidden emergency kill switch; Notify's running state gates presentation,
12
- * not capture. Android-only; a no-op elsewhere. Started by `engine.start()`.
13
- */
1
+ /** One engine-owned notification policy: capture remains independent of presentation. */
2
+ import {Platform} from "react-native"
14
3
  import {shallow} from "zustand/shallow"
15
4
  import CrustModule from "@mentra/crust"
5
+ import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
6
+ import type {PhoneNotificationEvent} from "@mentra/crust"
16
7
 
17
8
  import {useSettingsStore, SETTINGS} from "../stores/settings"
9
+ import {useGlassesStore, isGlassesReady} from "../stores/glasses"
10
+ import {DeviceTypes} from "../types/enums"
11
+ import {getModelCapabilities} from "../types/hardware"
12
+ import {nativeNotificationConfig} from "./NativeNotificationPolicy"
18
13
 
19
- let unsubscribe: (() => void) | null = null
14
+ let unsubscribers: Array<() => void> = []
15
+ let presentationActive = false
20
16
 
21
- function pushConfig(): void {
22
- const s = useSettingsStore.getState()
23
- const listenerEnabled = Boolean(s.getSetting(SETTINGS.android_notification_listener_enabled.key))
24
- const blocklist = s.getSetting(SETTINGS.notifications_blocklist.key)
25
- CrustModule.setNotificationConfig(listenerEnabled, Array.isArray(blocklist) ? blocklist : []).catch((err: unknown) =>
26
- console.warn(`PhoneNotificationsSync: setNotificationConfig failed: ${(err as Error)?.message ?? err}`),
17
+ export function nativeNotificationCapabilities() {
18
+ const supported = Boolean(
19
+ getModelCapabilities(useGlassesStore.getState().deviceModel as DeviceTypes)?.hasNativeNotifications,
20
+ )
21
+ return {
22
+ supported,
23
+ source: Platform.OS === "ios" ? ("ancs" as const) : ("phone" as const),
24
+ contentAccess:
25
+ Platform.OS === "android" ? ("full" as const) : supported ? ("app_identity" as const) : ("unsupported" as const),
26
+ perAppFiltering: Platform.OS === "android",
27
+ removal: false, // The verified G2 protocol does not yet include a removal command.
28
+ }
29
+ }
30
+
31
+ function desiredConfig() {
32
+ const settings = useSettingsStore.getState()
33
+ return nativeNotificationConfig({
34
+ supported: nativeNotificationCapabilities().supported,
35
+ selected: Boolean(settings.getSetting(SETTINGS.native_notifications_enabled.key)),
36
+ presentationActive,
37
+ sourceEnabled:
38
+ Platform.OS !== "android" || Boolean(settings.getSetting(SETTINGS.android_notification_listener_enabled.key)),
39
+ autoDisplay: Boolean(settings.getSetting(SETTINGS.native_notifications_auto_display.key)),
40
+ durationSeconds: Number(settings.getSetting(SETTINGS.native_notifications_duration.key)),
41
+ doNotDisturb: Boolean(settings.getSetting(SETTINGS.native_notifications_do_not_disturb.key)),
42
+ blockedApps: Platform.OS === "android" ? settings.getSetting(SETTINGS.notifications_blocklist.key) : [],
43
+ })
44
+ }
45
+
46
+ function pushNativeConfig(): void {
47
+ if (!nativeNotificationCapabilities().supported) return
48
+ void BluetoothSdk.configureNativeNotifications(desiredConfig()).catch((error: unknown) => {
49
+ if (isGlassesReady(useGlassesStore.getState().connection)) {
50
+ console.warn("PhoneNotificationsSync: native notification configuration failed", error)
51
+ }
52
+ })
53
+ }
54
+
55
+ /** Hosts activate their presentation owner (Notify in the Mentra App) through this seam. */
56
+ export function setPhoneNotificationPresentationActive(active: boolean): void {
57
+ if (active === presentationActive) return
58
+ presentationActive = active
59
+ pushNativeConfig()
60
+ }
61
+
62
+ export function usesNativeNotificationPresentation(): boolean {
63
+ return (
64
+ nativeNotificationCapabilities().supported &&
65
+ presentationActive &&
66
+ Boolean(useSettingsStore.getState().getSetting(SETTINGS.native_notifications_enabled.key))
27
67
  )
28
68
  }
29
69
 
70
+ /** Return true when the selected firmware presentation owns this event, including quiet/history mode. */
71
+ export async function presentNativePhoneNotification(event: PhoneNotificationEvent): Promise<boolean> {
72
+ if (!usesNativeNotificationPresentation()) return false
73
+ if (!desiredConfig().enabled) return true
74
+ if (Platform.OS !== "android") return true // G2 receives ANCS directly; never upload metadata back.
75
+ const blocked = useSettingsStore.getState().getSetting(SETTINGS.notifications_blocklist.key)
76
+ if (Array.isArray(blocked) && blocked.includes(event.packageName)) return true
77
+ if (!isGlassesReady(useGlassesStore.getState().connection)) return true
78
+ await BluetoothSdk.sendPhoneNotification({
79
+ notificationId: String(event.notificationId ?? ""),
80
+ packageName: String(event.packageName ?? ""),
81
+ appName: String(event.app ?? ""),
82
+ title: String(event.title ?? ""),
83
+ subtitle: "",
84
+ body: String(event.content ?? ""),
85
+ timestampMs: Number(event.timestamp ?? Date.now()),
86
+ action: 0,
87
+ })
88
+ return true
89
+ }
90
+
91
+ function pushConfig(): void {
92
+ const settings = useSettingsStore.getState()
93
+ if (Platform.OS === "android") {
94
+ const enabled = Boolean(settings.getSetting(SETTINGS.android_notification_listener_enabled.key))
95
+ const blocklist = settings.getSetting(SETTINGS.notifications_blocklist.key)
96
+ void CrustModule.setNotificationConfig(enabled, Array.isArray(blocklist) ? blocklist : []).catch((error: unknown) =>
97
+ console.warn("PhoneNotificationsSync: listener configuration failed", error),
98
+ )
99
+ }
100
+ pushNativeConfig()
101
+ }
102
+
30
103
  export function startPhoneNotificationsSync(): void {
31
- if (unsubscribe) return
32
- pushConfig() // initial sync so the listener has the current config
33
- unsubscribe = useSettingsStore.subscribe(
34
- (state) => ({
35
- listenerEnabled: state.getSetting(SETTINGS.android_notification_listener_enabled.key),
36
- blocklist: state.getSetting(SETTINGS.notifications_blocklist.key),
104
+ if (unsubscribers.length) return
105
+ pushConfig()
106
+ unsubscribers = [
107
+ useSettingsStore.subscribe(
108
+ (state) =>
109
+ [
110
+ SETTINGS.android_notification_listener_enabled,
111
+ SETTINGS.notifications_blocklist,
112
+ SETTINGS.native_notifications_enabled,
113
+ SETTINGS.native_notifications_auto_display,
114
+ SETTINGS.native_notifications_duration,
115
+ SETTINGS.native_notifications_do_not_disturb,
116
+ ].map((setting) => state.getSetting(setting.key)),
117
+ pushConfig,
118
+ {equalityFn: shallow},
119
+ ),
120
+ useGlassesStore.subscribe((state) => ({model: state.deviceModel, connection: state.connection}), pushNativeConfig, {
121
+ equalityFn: shallow,
37
122
  }),
38
- () => pushConfig(),
39
- {equalityFn: shallow},
40
- )
123
+ ]
41
124
  }
42
125
 
43
126
  export function stopPhoneNotificationsSync(): void {
44
- unsubscribe?.()
45
- unsubscribe = null
127
+ setPhoneNotificationPresentationActive(false)
128
+ unsubscribers.forEach((unsubscribe) => unsubscribe())
129
+ unsubscribers = []
46
130
  }
@@ -0,0 +1,25 @@
1
+ import type {IslandConfigValues} from "../runtime/bootstrap"
2
+
3
+ export type CloudEndpoints = {core?: string; runtime: string}
4
+
5
+ /** Explicit reconnect pins win; clearing a pin resumes the live host resolver. */
6
+ export function resolveCloudEndpoints(config: IslandConfigValues, override: CloudEndpoints | null): CloudEndpoints {
7
+ if (override) return override
8
+ if (config.resolveCloudEndpoints) return config.resolveCloudEndpoints()
9
+ const runtime = config.runtimeUrl === null ? "" : config.runtimeUrl?.trim() || "http://localhost:3001"
10
+ if (!runtime) throw new Error("cloudClient: Runtime endpoint is not configured")
11
+ const core = config.coreUrl === null ? undefined : config.coreUrl?.trim() || "http://localhost:3000"
12
+ return {...(core ? {core} : {}), runtime}
13
+ }
14
+
15
+ export function scopeCloudUrlOverrides(
16
+ current: {scope: unknown; core?: string; runtime?: string},
17
+ scope: string,
18
+ update: {core?: string; runtime?: string},
19
+ ): {core: string; runtime: string} {
20
+ const sameScope = current.scope === scope || (!current.scope && scope === "consumer")
21
+ return {
22
+ core: update.core ?? (sameScope ? (current.core ?? "") : ""),
23
+ runtime: update.runtime ?? (sameScope ? (current.runtime ?? "") : ""),
24
+ }
25
+ }
@@ -48,6 +48,6 @@ export function resolveDeploymentAwareOtaManifestPolicy({
48
48
  // A selected deployment is authoritative for every glasses generation. An
49
49
  // explicit null is a deliberate OTA-off policy, not permission to inherit a
50
50
  // device-reported URL or Mentra's legacy public default.
51
- if (hostPolicyConfigured) return trimmed(policy.hostReleasePin)
51
+ if (hostPolicyConfigured) return trimmed(policy.developerOverride) || trimmed(policy.hostReleasePin)
52
52
  return resolveOtaManifestPolicy(policy)
53
53
  }
@@ -22,7 +22,8 @@ function getHostReleasePin(): string | null {
22
22
  }
23
23
 
24
24
  function hasDeploymentOtaPolicy(): boolean {
25
- return getConfigValues().otaManifestUrl !== undefined
25
+ const config = getConfigValues()
26
+ return config.otaManifestUrl !== undefined && !config.allowLegacyOtaFallback
26
27
  }
27
28
 
28
29
  function getEmbeddedEngineReleasePin(): string | null {
@@ -166,6 +166,36 @@ export const SETTINGS: Record<string, Setting> = {
166
166
  saveOnServer: false,
167
167
  persist: true,
168
168
  },
169
+ // Select firmware-owned presentation instead of the Mentra card. Notify still
170
+ // controls whether presentation is running on either phone platform.
171
+ native_notifications_enabled: {
172
+ key: "native_notifications_enabled",
173
+ defaultValue: () => false,
174
+ writable: true,
175
+ saveOnServer: false,
176
+ persist: true,
177
+ },
178
+ native_notifications_auto_display: {
179
+ key: "native_notifications_auto_display",
180
+ defaultValue: () => true,
181
+ writable: true,
182
+ saveOnServer: false,
183
+ persist: true,
184
+ },
185
+ native_notifications_duration: {
186
+ key: "native_notifications_duration",
187
+ defaultValue: () => 5,
188
+ writable: true,
189
+ saveOnServer: false,
190
+ persist: true,
191
+ },
192
+ native_notifications_do_not_disturb: {
193
+ key: "native_notifications_do_not_disturb",
194
+ defaultValue: () => false,
195
+ writable: true,
196
+ saveOnServer: false,
197
+ persist: true,
198
+ },
169
199
  china_deployment: {
170
200
  key: "china_deployment",
171
201
  defaultValue: () => (process.env.EXPO_PUBLIC_DEPLOYMENT_REGION === "china" ? true : false),
@@ -196,6 +226,14 @@ export const SETTINGS: Record<string, Setting> = {
196
226
  persist: true,
197
227
  resetOnBuildEnvChange: true,
198
228
  },
229
+ cloud_url_deployment: {
230
+ key: "cloud_url_deployment",
231
+ defaultValue: () => "",
232
+ writable: true,
233
+ saveOnServer: false,
234
+ persist: true,
235
+ resetOnBuildEnvChange: true,
236
+ },
199
237
  // Bookmarked Cloud V2 endpoint pairs. Each entry is {label, coreUrl,
200
238
  // runtimeUrl} — core + runtime are saved together because they are always
201
239
  // applied as a matched set (presets fill both; Save & Test verifies both).
@@ -793,13 +831,10 @@ export interface SettingsState {
793
831
  }
794
832
 
795
833
  const getDefaultSettings = () =>
796
- Object.keys(SETTINGS).reduce(
797
- (acc, key) => {
798
- acc[key] = SETTINGS[key].defaultValue()
799
- return acc
800
- },
801
- {} as Record<string, any>,
802
- )
834
+ Object.keys(SETTINGS).reduce((acc, key) => {
835
+ acc[key] = SETTINGS[key].defaultValue()
836
+ return acc
837
+ }, {} as Record<string, any>)
803
838
 
804
839
  // Single-flight for loadAllSettings: the host fires it at module load and
805
840
  // engine.start()'s device-store hydration awaits it — without the memo the
@@ -901,12 +936,13 @@ export const useSettingsStore = create<SettingsState>()(
901
936
  }
902
937
  // console.log("SETTINGS: SET MANY LOCALLY: ", settingsToLoad)
903
938
 
904
- set((state) => ({
905
- settings: {...state.settings, ...settingsToLoad},
906
- }))
907
-
908
- // save to storage:
909
- await Promise.all(Object.entries(settingsToLoad).map(([key, value]) => storage.save(key, value)))
939
+ // MMKV writes are synchronous Results, not rejecting Promises. Check
940
+ // each result before publishing the new in-memory settings.
941
+ for (const [key, value] of Object.entries(settingsToLoad)) {
942
+ const saved = storage.save(key, value)
943
+ if (saved.is_error()) throw saved.error
944
+ }
945
+ set((state) => ({settings: {...state.settings, ...settingsToLoad}}))
910
946
  })
911
947
  },
912
948
  // loads any preferences that have been changed from the default and saved to DISK!
@@ -88,4 +88,5 @@ export const evenRealitiesG2: Capabilities = {
88
88
  // Dashboard - G2 renders Even Realities' native dashboard in firmware, so
89
89
  // MentraOS does not manage the dashboard or expose dashboard settings for it
90
90
  hasNativeDashboard: true,
91
+ hasNativeNotifications: true,
91
92
  };
@@ -122,6 +122,8 @@ export interface PowerCapabilities {
122
122
  * Complete information about what hardware a device has
123
123
  */
124
124
  export interface Capabilities {
125
+ /** Firmware-owned notification history and popups; content relay is separate. */
126
+ hasNativeNotifications?: boolean;
125
127
  modelName: string;
126
128
 
127
129
  // Camera capabilities