@ateam-ai-mobile/expo 0.1.19 → 0.1.21

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 (76) hide show
  1. package/dist/aiLive/provider.d.mts +20 -0
  2. package/dist/aiLive/provider.d.ts +20 -0
  3. package/dist/aiLive/provider.js +400 -0
  4. package/dist/aiLive/provider.js.map +1 -0
  5. package/dist/aiLive/provider.mjs +15 -0
  6. package/dist/aiLive/provider.mjs.map +1 -0
  7. package/dist/bootstrap.js +350 -3
  8. package/dist/bootstrap.js.map +1 -1
  9. package/dist/bootstrap.mjs +17 -12
  10. package/dist/bootstrap.mjs.map +1 -1
  11. package/dist/capabilities/index.d.mts +40 -1
  12. package/dist/capabilities/index.d.ts +40 -1
  13. package/dist/capabilities/index.js +50 -14
  14. package/dist/capabilities/index.js.map +1 -1
  15. package/dist/capabilities/index.mjs +49 -32
  16. package/dist/capabilities/index.mjs.map +1 -1
  17. package/dist/{chunk-42FP5DTO.mjs → chunk-2ZQEQJQ6.mjs} +1 -1
  18. package/dist/chunk-2ZQEQJQ6.mjs.map +1 -0
  19. package/dist/chunk-AE55BKYV.mjs +24 -0
  20. package/dist/chunk-AE55BKYV.mjs.map +1 -0
  21. package/dist/chunk-BAZFMTCC.mjs +815 -0
  22. package/dist/chunk-BAZFMTCC.mjs.map +1 -0
  23. package/dist/{chunk-Z5AI2SXX.mjs → chunk-C62NQA4F.mjs} +203 -1004
  24. package/dist/chunk-C62NQA4F.mjs.map +1 -0
  25. package/dist/{chunk-DE6Y4VRI.mjs → chunk-GA7DS3PA.mjs} +1 -1
  26. package/dist/{chunk-DE6Y4VRI.mjs.map → chunk-GA7DS3PA.mjs.map} +1 -1
  27. package/dist/chunk-GW7YGXOW.mjs +47 -0
  28. package/dist/chunk-GW7YGXOW.mjs.map +1 -0
  29. package/dist/chunk-JUBLG3PN.mjs +351 -0
  30. package/dist/chunk-JUBLG3PN.mjs.map +1 -0
  31. package/dist/{chunk-Z3P7RUCC.mjs → chunk-OEX4OKYQ.mjs} +13 -13
  32. package/dist/{chunk-6JX222YX.mjs → chunk-WQK6PUBN.mjs} +9 -2
  33. package/dist/chunk-WQK6PUBN.mjs.map +1 -0
  34. package/dist/device/createDeviceProvider.js.map +1 -1
  35. package/dist/device/createDeviceProvider.mjs +7 -6
  36. package/dist/device-bridge/index.mjs +8 -6
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.mjs +1 -1
  39. package/dist/push/index.d.mts +24 -0
  40. package/dist/push/index.d.ts +24 -0
  41. package/dist/push/index.js +224 -0
  42. package/dist/push/index.js.map +1 -0
  43. package/dist/push/index.mjs +174 -0
  44. package/dist/push/index.mjs.map +1 -0
  45. package/dist/push/silentSync.d.mts +8 -0
  46. package/dist/push/silentSync.d.ts +8 -0
  47. package/dist/push/silentSync.js +1075 -0
  48. package/dist/push/silentSync.js.map +1 -0
  49. package/dist/push/silentSync.mjs +67 -0
  50. package/dist/push/silentSync.mjs.map +1 -0
  51. package/dist/vision/index.d.mts +7 -1
  52. package/dist/vision/index.d.ts +7 -1
  53. package/dist/vision/index.js +8 -0
  54. package/dist/vision/index.js.map +1 -1
  55. package/dist/vision/index.mjs +3 -1
  56. package/ios/AdaVisionModule.swift +44 -1
  57. package/package.json +28 -6
  58. package/src/aiLive/provider.ts +242 -0
  59. package/src/aiLive/streamProtocol.ts +90 -0
  60. package/src/bootstrap.ts +7 -1
  61. package/src/capabilities/choices.ts +65 -0
  62. package/src/capabilities/config.ts +5 -4
  63. package/src/capabilities/index.ts +2 -0
  64. package/src/capabilities/picturesToAi.ts +40 -0
  65. package/src/capabilities/store.ts +38 -37
  66. package/src/index.ts +3 -0
  67. package/src/push/index.ts +18 -0
  68. package/src/push/messaging.ts +49 -0
  69. package/src/push/registration.ts +31 -0
  70. package/src/push/silentSync.ts +83 -0
  71. package/src/push/usePushNotifications.ts +224 -0
  72. package/src/vision/index.ts +14 -0
  73. package/dist/chunk-42FP5DTO.mjs.map +0 -1
  74. package/dist/chunk-6JX222YX.mjs.map +0 -1
  75. package/dist/chunk-Z5AI2SXX.mjs.map +0 -1
  76. /package/dist/{chunk-Z3P7RUCC.mjs.map → chunk-OEX4OKYQ.mjs.map} +0 -0
package/dist/index.mjs CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  parseWalkJsonl,
10
10
  setupExpoProviders,
11
11
  visualEngine
12
- } from "./chunk-42FP5DTO.mjs";
12
+ } from "./chunk-2ZQEQJQ6.mjs";
13
13
  import {
14
14
  cameraTasks
15
15
  } from "./chunk-6LXF3JVX.mjs";
@@ -0,0 +1,24 @@
1
+ import { PushRegistration } from '@ateam-ai-mobile/core';
2
+
3
+ interface PushNotificationsOptions {
4
+ /** A tapped push that names no surface or prompt — e.g. show your chat screen. */
5
+ onOpenApp?: () => void;
6
+ /**
7
+ * Set the expo-notifications handler so notifications also show while the app
8
+ * is open (banner, list, sound, badge) — this covers the local notifications
9
+ * plugins schedule. Default true; pass false if your app sets its own.
10
+ */
11
+ foregroundHandler?: boolean;
12
+ }
13
+ interface UsePushNotificationsOptions extends PushNotificationsOptions {
14
+ /** Push runs while true — pass "signed in". */
15
+ enabled: boolean;
16
+ }
17
+ /** Push while `enabled`; stops (listeners off) when it turns false or on unmount. */
18
+ declare function usePushNotifications({ enabled, onOpenApp, foregroundHandler }: UsePushNotificationsOptions): void;
19
+ /** Start push on this device; returns stop(). Call while signed in. No Firebase → no-op. */
20
+ declare function startPushNotifications(opts?: PushNotificationsOptions): () => void;
21
+
22
+ declare const pushRegistration: PushRegistration;
23
+
24
+ export { type PushNotificationsOptions, type UsePushNotificationsOptions, pushRegistration, startPushNotifications, usePushNotifications };
@@ -0,0 +1,24 @@
1
+ import { PushRegistration } from '@ateam-ai-mobile/core';
2
+
3
+ interface PushNotificationsOptions {
4
+ /** A tapped push that names no surface or prompt — e.g. show your chat screen. */
5
+ onOpenApp?: () => void;
6
+ /**
7
+ * Set the expo-notifications handler so notifications also show while the app
8
+ * is open (banner, list, sound, badge) — this covers the local notifications
9
+ * plugins schedule. Default true; pass false if your app sets its own.
10
+ */
11
+ foregroundHandler?: boolean;
12
+ }
13
+ interface UsePushNotificationsOptions extends PushNotificationsOptions {
14
+ /** Push runs while true — pass "signed in". */
15
+ enabled: boolean;
16
+ }
17
+ /** Push while `enabled`; stops (listeners off) when it turns false or on unmount. */
18
+ declare function usePushNotifications({ enabled, onOpenApp, foregroundHandler }: UsePushNotificationsOptions): void;
19
+ /** Start push on this device; returns stop(). Call while signed in. No Firebase → no-op. */
20
+ declare function startPushNotifications(opts?: PushNotificationsOptions): () => void;
21
+
22
+ declare const pushRegistration: PushRegistration;
23
+
24
+ export { type PushNotificationsOptions, type UsePushNotificationsOptions, pushRegistration, startPushNotifications, usePushNotifications };
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/push/index.ts
31
+ var push_exports = {};
32
+ __export(push_exports, {
33
+ pushRegistration: () => pushRegistration,
34
+ startPushNotifications: () => startPushNotifications,
35
+ usePushNotifications: () => usePushNotifications
36
+ });
37
+ module.exports = __toCommonJS(push_exports);
38
+
39
+ // src/push/usePushNotifications.ts
40
+ var import_react = require("react");
41
+ var import_react_native2 = require("react-native");
42
+ var Notifications2 = __toESM(require("expo-notifications"));
43
+ var Device = __toESM(require("expo-device"));
44
+ var import_core2 = require("@ateam-ai-mobile/core");
45
+ var import_ui = require("@ateam-ai-mobile/ui");
46
+
47
+ // src/push/messaging.ts
48
+ var probed = false;
49
+ var messaging = null;
50
+ function getMessaging() {
51
+ if (probed) return messaging;
52
+ probed = true;
53
+ try {
54
+ const candidate = require("@react-native-firebase/messaging").default;
55
+ candidate();
56
+ messaging = candidate;
57
+ } catch {
58
+ console.warn("[adas/push] @react-native-firebase/messaging native module unavailable \u2014 push disabled (Expo Go?)");
59
+ }
60
+ return messaging;
61
+ }
62
+
63
+ // src/push/registration.ts
64
+ var import_react_native = require("react-native");
65
+ var Notifications = __toESM(require("expo-notifications"));
66
+ var import_core = require("@ateam-ai-mobile/core");
67
+ var lifecycle = (0, import_core.createPushRegistration)({
68
+ // Adapt to the lifecycle's void contract — the notif-api returns a body it doesn't need.
69
+ registerDevice: async (token) => {
70
+ await (0, import_core.registerPushDevice)(token, import_react_native.Platform.OS === "ios" ? "ios" : "android");
71
+ },
72
+ unregisterDevice: async (token) => {
73
+ await (0, import_core.unregisterPushDevice)(token);
74
+ }
75
+ });
76
+ var pushRegistration = {
77
+ ...lifecycle,
78
+ async unregister() {
79
+ await lifecycle.unregister();
80
+ Promise.resolve().then(() => Notifications.dismissAllNotificationsAsync()).catch(
81
+ (e) => console.warn("[adas/push] clearing delivered notifications failed:", e?.message || e)
82
+ );
83
+ }
84
+ };
85
+
86
+ // src/push/usePushNotifications.ts
87
+ function usePushNotifications({ enabled, onOpenApp, foregroundHandler }) {
88
+ const onOpenAppRef = (0, import_react.useRef)(onOpenApp);
89
+ onOpenAppRef.current = onOpenApp;
90
+ (0, import_react.useEffect)(() => {
91
+ if (!enabled) return;
92
+ return startPushNotifications({ onOpenApp: () => onOpenAppRef.current?.(), foregroundHandler });
93
+ }, [enabled, foregroundHandler]);
94
+ }
95
+ var initialTapHandled = false;
96
+ var launchGeneration = pushRegistration.generation();
97
+ var handlerSet = false;
98
+ var activeStop = null;
99
+ function startPushNotifications(opts = {}) {
100
+ if (import_react_native2.Platform.OS === "web") return () => {
101
+ };
102
+ if (opts.foregroundHandler !== false) setForegroundHandler();
103
+ const messaging2 = getMessaging();
104
+ if (!messaging2) return () => {
105
+ };
106
+ activeStop?.();
107
+ const startedIn = pushRegistration.generation();
108
+ let stopped = false;
109
+ const live = () => !stopped && pushRegistration.generation() === startedIn;
110
+ let unsubRefresh = null;
111
+ const routeTap = (msg) => {
112
+ if (live()) (0, import_ui.routePushTap)(msg?.data, opts.onOpenApp);
113
+ };
114
+ (async () => {
115
+ const token = await registerForPush(messaging2);
116
+ if (!token || !live()) return;
117
+ await pushRegistration.setToken(token);
118
+ if (!live()) return;
119
+ unsubRefresh = messaging2().onTokenRefresh((next) => {
120
+ if (live()) void pushRegistration.setToken(next);
121
+ });
122
+ const initial = await messaging2().getInitialNotification();
123
+ if (initial && !initialTapHandled && live() && startedIn === launchGeneration) {
124
+ initialTapHandled = true;
125
+ routeTap(initial);
126
+ }
127
+ })().catch((err) => console.warn("[adas/push] start failed:", err?.message || err));
128
+ const unsubMessage = messaging2().onMessage((msg) => {
129
+ const n = msg.notification;
130
+ if (!live()) return;
131
+ if (!n?.title && !n?.body) return;
132
+ import_ui.usePushBannerStore.getState().show({ title: n.title || "Ada", body: n.body || void 0, data: msg.data || {} });
133
+ });
134
+ const unsubOpened = messaging2().onNotificationOpenedApp(routeTap);
135
+ const stop = () => {
136
+ if (stopped) return;
137
+ stopped = true;
138
+ unsubRefresh?.();
139
+ unsubMessage();
140
+ unsubOpened();
141
+ import_ui.usePushBannerStore.getState().dismiss();
142
+ if (activeStop === stop) activeStop = null;
143
+ };
144
+ activeStop = stop;
145
+ return stop;
146
+ }
147
+ function setForegroundHandler() {
148
+ if (handlerSet) return;
149
+ handlerSet = true;
150
+ Notifications2.setNotificationHandler({
151
+ handleNotification: async () => ({
152
+ shouldShowAlert: true,
153
+ shouldPlaySound: true,
154
+ shouldSetBadge: true,
155
+ shouldShowBanner: true,
156
+ shouldShowList: true
157
+ })
158
+ });
159
+ }
160
+ async function registerForPush(messaging2) {
161
+ if (!Device.isDevice) {
162
+ console.warn("[adas/push] requires a physical device");
163
+ return null;
164
+ }
165
+ try {
166
+ if (import_react_native2.Platform.OS === "ios") {
167
+ const status = await messaging2().requestPermission();
168
+ const granted = status === messaging2.AuthorizationStatus.AUTHORIZED || status === messaging2.AuthorizationStatus.PROVISIONAL;
169
+ if (!granted) {
170
+ console.warn(`[adas/push] iOS permission not granted (status ${status})`);
171
+ return null;
172
+ }
173
+ } else if (import_react_native2.Platform.OS === "android") {
174
+ const { status: existing } = await Notifications2.getPermissionsAsync();
175
+ let final = existing;
176
+ if (existing !== "granted") {
177
+ const { status } = await Notifications2.requestPermissionsAsync();
178
+ final = status;
179
+ }
180
+ if (final !== "granted") {
181
+ console.warn(`[adas/push] Android permission not granted (${final})`);
182
+ return null;
183
+ }
184
+ await Notifications2.setNotificationChannelAsync("default", {
185
+ name: "Default",
186
+ importance: Notifications2.AndroidImportance.MAX,
187
+ vibrationPattern: [0, 250, 250, 250],
188
+ lightColor: import_core2.ada.orange
189
+ });
190
+ }
191
+ if (import_react_native2.Platform.OS === "ios") {
192
+ if (!messaging2().isDeviceRegisteredForRemoteMessages) {
193
+ await messaging2().registerDeviceForRemoteMessages();
194
+ }
195
+ if (!await waitForApnsToken(messaging2)) {
196
+ console.warn(
197
+ "[adas/push] APNs token never arrived \u2014 Firebase cannot mint an FCM token. Check the aps-environment entitlement and the APNs key in Firebase Console \u2192 Cloud Messaging \u2192 Apple app config."
198
+ );
199
+ }
200
+ }
201
+ const token = await messaging2().getToken();
202
+ console.log(`[adas/push] FCM token ${token ? "acquired" : "missing"}`);
203
+ return token || null;
204
+ } catch (err) {
205
+ console.error("[adas/push] register failed:", err);
206
+ return null;
207
+ }
208
+ }
209
+ async function waitForApnsToken(messaging2, maxMs = 5e3) {
210
+ const deadline = Date.now() + maxMs;
211
+ let token = await messaging2().getAPNSToken();
212
+ while (!token && Date.now() < deadline) {
213
+ await new Promise((r) => setTimeout(r, 250));
214
+ token = await messaging2().getAPNSToken();
215
+ }
216
+ return token || null;
217
+ }
218
+ // Annotate the CommonJS export names for ESM import in node:
219
+ 0 && (module.exports = {
220
+ pushRegistration,
221
+ startPushNotifications,
222
+ usePushNotifications
223
+ });
224
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/push/index.ts","../../src/push/usePushNotifications.ts","../../src/push/messaging.ts","../../src/push/registration.ts"],"sourcesContent":["/**\n * Push notifications on this device (FCM via @react-native-firebase/messaging,\n * an optional peer — without it push is off and nothing breaks).\n *\n * For a host app embedding A-Team:\n * - usePushNotifications({ enabled, onOpenApp }) — or startPushNotifications()\n * outside React — while signed in: permission, token → Core, incoming pushes;\n * - pushRegistration — the current token (getToken) and unregister() for your\n * sign-out teardown, run while the session token still exists;\n * - <PushBanner/> from @ateam-ai-mobile/ui — the drop-in in-app banner, or your\n * own UI on usePushBannerStore + routePushTap.\n * The background silent sync is its own subpath, @ateam-ai-mobile/expo/silent-push.\n *\n * A SUBPATH because it statically pulls expo-notifications and expo-device.\n */\nexport { usePushNotifications, startPushNotifications } from './usePushNotifications';\nexport type { UsePushNotificationsOptions, PushNotificationsOptions } from './usePushNotifications';\nexport { pushRegistration } from './registration';\n","/**\n * Push notifications on this device — the headless half.\n *\n * startPushNotifications() (or the usePushNotifications hook around it) asks\n * for permission, gets this device's FCM token, registers it with Core through\n * pushRegistration, re-registers when Firebase refreshes it, and handles what\n * arrives:\n * - app open: a push with a visible notification goes to the in-app banner\n * (usePushBannerStore → <PushBanner/> from @ateam-ai-mobile/ui); data-only\n * pushes (silent syncs) are not shown;\n * - tapped in the background, or the tap that cold-started the app:\n * routePushTap (surface → prompt → the host's onOpenApp).\n * Run it only while signed in — registering needs the session token. Sign-out\n * unregisters through pushRegistration.unregister() in the host's teardown;\n * from that moment this start adopts no token and routes no tap, even before\n * the host's UI stops it. One push owner per app: a second start replaces the\n * first.\n *\n * Payload from Core's notif-router → FCM:\n * notification: { title, body }\n * data: { jobId, skillSlug, messageId, surfaceId?, pluginId?, prompt? }\n * In the background the OS shows the system banner itself.\n *\n * Moved from ateam-mobile's usePushNotifications (2026-10-29). Changed on the\n * way: the expo-notifications foreground handler is set on start, not at import\n * (an embedding app with its own passes foregroundHandler: false); a sign-out\n * while the token was being fetched or registered no longer registers the\n * signed-out phone or leaves a refresh listener behind; stopping clears the\n * in-app banner; data-only pushes no longer pop an empty banner; the tap that\n * opened the app is routed once, not again after every sign-in.\n */\nimport { useEffect, useRef } from 'react';\nimport { Platform } from 'react-native';\nimport * as Notifications from 'expo-notifications';\nimport * as Device from 'expo-device';\nimport { ada } from '@ateam-ai-mobile/core';\nimport { routePushTap, usePushBannerStore } from '@ateam-ai-mobile/ui';\nimport { getMessaging, type Messaging, type RemoteMessage } from './messaging';\nimport { pushRegistration } from './registration';\n\nexport interface PushNotificationsOptions {\n /** A tapped push that names no surface or prompt — e.g. show your chat screen. */\n onOpenApp?: () => void;\n /**\n * Set the expo-notifications handler so notifications also show while the app\n * is open (banner, list, sound, badge) — this covers the local notifications\n * plugins schedule. Default true; pass false if your app sets its own.\n */\n foregroundHandler?: boolean;\n}\n\nexport interface UsePushNotificationsOptions extends PushNotificationsOptions {\n /** Push runs while true — pass \"signed in\". */\n enabled: boolean;\n}\n\n/** Push while `enabled`; stops (listeners off) when it turns false or on unmount. */\nexport function usePushNotifications({ enabled, onOpenApp, foregroundHandler }: UsePushNotificationsOptions): void {\n const onOpenAppRef = useRef(onOpenApp);\n onOpenAppRef.current = onOpenApp;\n\n useEffect(() => {\n if (!enabled) return;\n return startPushNotifications({ onOpenApp: () => onOpenAppRef.current?.(), foregroundHandler });\n }, [enabled, foregroundHandler]);\n}\n\n// The tap that cold-started the app is routed once per launch — Firebase keeps\n// returning it, and a sign-out + sign-in starts push again — and never after a\n// sign-out: it was a push for the user signed in at launch.\nlet initialTapHandled = false;\nconst launchGeneration = pushRegistration.generation();\nlet handlerSet = false;\n// The running start's stop — one push owner per app.\nlet activeStop: (() => void) | null = null;\n\n/** Start push on this device; returns stop(). Call while signed in. No Firebase → no-op. */\nexport function startPushNotifications(opts: PushNotificationsOptions = {}): () => void {\n if (Platform.OS === 'web') return () => {};\n // Before the Firebase check: it also shows the local notifications plugins\n // schedule, which work without Firebase.\n if (opts.foregroundHandler !== false) setForegroundHandler();\n const messaging = getMessaging();\n if (!messaging) return () => {};\n activeStop?.();\n\n // Live = not stopped AND no sign-out since this start began. The host's\n // sign-out unregisters first and stops us only when its UI catches up; a\n // token or tap arriving in between belongs to the user who left.\n const startedIn = pushRegistration.generation();\n let stopped = false;\n const live = () => !stopped && pushRegistration.generation() === startedIn;\n let unsubRefresh: (() => void) | null = null;\n const routeTap = (msg: RemoteMessage | null) => {\n if (live()) routePushTap(msg?.data, opts.onOpenApp);\n };\n\n (async () => {\n const token = await registerForPush(messaging);\n if (!token || !live()) return;\n await pushRegistration.setToken(token);\n // Signed out while registering: the teardown already unregistered this\n // token — don't listen for refreshes as a signed-out user.\n if (!live()) return;\n unsubRefresh = messaging().onTokenRefresh((next) => {\n if (live()) void pushRegistration.setToken(next);\n });\n\n const initial = await messaging().getInitialNotification();\n if (initial && !initialTapHandled && live() && startedIn === launchGeneration) {\n initialTapHandled = true;\n routeTap(initial);\n }\n })().catch((err) => console.warn('[adas/push] start failed:', err?.message || err));\n\n // App open: Firebase delivers but the OS shows nothing — use the in-app banner.\n const unsubMessage = messaging().onMessage((msg) => {\n const n = msg.notification;\n if (!live()) return;\n if (!n?.title && !n?.body) return; // data-only (e.g. a silent sync) — nothing to show\n usePushBannerStore.getState().show({ title: n.title || 'Ada', body: n.body || undefined, data: msg.data || {} });\n });\n\n // App brought back from the background by a tap.\n const unsubOpened = messaging().onNotificationOpenedApp(routeTap);\n\n const stop = () => {\n if (stopped) return;\n stopped = true;\n unsubRefresh?.();\n unsubMessage();\n unsubOpened();\n // A banner shown for the user who is leaving must not stay tappable.\n usePushBannerStore.getState().dismiss();\n if (activeStop === stop) activeStop = null;\n };\n activeStop = stop;\n return stop;\n}\n\nfunction setForegroundHandler(): void {\n if (handlerSet) return;\n handlerSet = true;\n Notifications.setNotificationHandler({\n handleNotification: async () => ({\n shouldShowAlert: true,\n shouldPlaySound: true,\n shouldSetBadge: true,\n shouldShowBanner: true,\n shouldShowList: true,\n }),\n });\n}\n\nasync function registerForPush(messaging: Messaging): Promise<string | null> {\n if (!Device.isDevice) {\n console.warn('[adas/push] requires a physical device');\n return null;\n }\n\n try {\n if (Platform.OS === 'ios') {\n const status = await messaging().requestPermission();\n const granted =\n status === messaging.AuthorizationStatus.AUTHORIZED ||\n status === messaging.AuthorizationStatus.PROVISIONAL;\n if (!granted) {\n console.warn(`[adas/push] iOS permission not granted (status ${status})`);\n return null;\n }\n } else if (Platform.OS === 'android') {\n const { status: existing } = await Notifications.getPermissionsAsync();\n let final = existing;\n if (existing !== 'granted') {\n const { status } = await Notifications.requestPermissionsAsync();\n final = status;\n }\n if (final !== 'granted') {\n console.warn(`[adas/push] Android permission not granted (${final})`);\n return null;\n }\n await Notifications.setNotificationChannelAsync('default', {\n name: 'Default',\n importance: Notifications.AndroidImportance.MAX,\n vibrationPattern: [0, 250, 250, 250],\n lightColor: ada.orange,\n });\n }\n\n // iOS: register with APNs and WAIT for the APNs token before asking\n // Firebase for an FCM token. Calling getToken() before the APNs token\n // is available can return a stale/non-bridged token that the server\n // can't deliver to.\n if (Platform.OS === 'ios') {\n if (!messaging().isDeviceRegisteredForRemoteMessages) {\n await messaging().registerDeviceForRemoteMessages();\n }\n if (!(await waitForApnsToken(messaging))) {\n console.warn(\n '[adas/push] APNs token never arrived — Firebase cannot mint an FCM token. Check the aps-environment entitlement and the APNs key in Firebase Console → Cloud Messaging → Apple app config.',\n );\n }\n }\n\n const token = await messaging().getToken();\n console.log(`[adas/push] FCM token ${token ? 'acquired' : 'missing'}`);\n return token || null;\n } catch (err) {\n console.error('[adas/push] register failed:', err);\n return null;\n }\n}\n\n/** Polls getAPNSToken() up to ~5s. iOS sometimes needs a tick after\n * registerDeviceForRemoteMessages() returns before the token is available. */\nasync function waitForApnsToken(messaging: Messaging, maxMs = 5000): Promise<string | null> {\n const deadline = Date.now() + maxMs;\n let token = await messaging().getAPNSToken();\n while (!token && Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, 250));\n token = await messaging().getAPNSToken();\n }\n return token || null;\n}\n","/**\n * Firebase Cloud Messaging, if this build has it.\n *\n * @react-native-firebase/messaging is an optional peer: in Expo Go (or a host\n * without Firebase) the JS module may exist but its native module does not, so\n * we probe by calling messaging() once. No native module → null, and push is\n * off (the hook and the background sync become no-ops). Probed on first use,\n * not at import.\n */\n\n/** The slice of the RNFB messaging API the SDK uses. */\nexport interface RemoteMessage {\n notification?: { title?: string; body?: string };\n data?: Record<string, unknown>;\n}\n\nexport interface MessagingInstance {\n requestPermission(): Promise<number>;\n isDeviceRegisteredForRemoteMessages: boolean;\n registerDeviceForRemoteMessages(): Promise<void>;\n getAPNSToken(): Promise<string | null>;\n getToken(): Promise<string>;\n onTokenRefresh(cb: (token: string) => void): () => void;\n onMessage(cb: (msg: RemoteMessage) => void | Promise<void>): () => void;\n onNotificationOpenedApp(cb: (msg: RemoteMessage) => void): () => void;\n getInitialNotification(): Promise<RemoteMessage | null>;\n setBackgroundMessageHandler(cb: (msg: RemoteMessage) => Promise<void>): void;\n}\n\nexport interface Messaging {\n (): MessagingInstance;\n AuthorizationStatus: { AUTHORIZED: number; PROVISIONAL: number };\n}\n\nlet probed = false;\nlet messaging: Messaging | null = null;\n\nexport function getMessaging(): Messaging | null {\n if (probed) return messaging;\n probed = true;\n try {\n const candidate = require('@react-native-firebase/messaging').default as Messaging;\n candidate(); // throws \"Native module RNFBAppModule not found\" in Expo Go\n messaging = candidate;\n } catch {\n console.warn('[adas/push] @react-native-firebase/messaging native module unavailable — push disabled (Expo Go?)');\n }\n return messaging;\n}\n","/**\n * This device's push registration with Core — the one owner of the current\n * push token. The lifecycle (register, re-register on refresh, unregister on\n * sign-out, the sign-out count) is core's createPushRegistration; this wires it\n * to Core's notif-api with the platform. usePushNotifications feeds it tokens;\n * the host's sign-out teardown calls pushRegistration.unregister() while the\n * session token still exists. getToken() answers \"is push registered on this\n * device?\".\n */\nimport { Platform } from 'react-native';\nimport * as Notifications from 'expo-notifications';\nimport { createPushRegistration, registerPushDevice, unregisterPushDevice, type PushRegistration } from '@ateam-ai-mobile/core';\n\nconst lifecycle = createPushRegistration({\n // Adapt to the lifecycle's void contract — the notif-api returns a body it doesn't need.\n registerDevice: async (token) => { await registerPushDevice(token, Platform.OS === 'ios' ? 'ios' : 'android'); },\n unregisterDevice: async (token) => { await unregisterPushDevice(token); },\n});\n\nexport const pushRegistration: PushRegistration = {\n ...lifecycle,\n async unregister() {\n await lifecycle.unregister();\n // Notifications already in the list were for the user who is leaving, and\n // a push carries no recipient: tapped later, one would act in the next\n // user's chat. Best-effort, never blocks sign-out.\n Promise.resolve().then(() => Notifications.dismissAllNotificationsAsync()).catch((e: any) =>\n console.warn('[adas/push] clearing delivered notifications failed:', e?.message || e),\n );\n },\n};\n"],"mappings":"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","names":["import_react_native","Notifications","import_core","messaging"]}
@@ -0,0 +1,174 @@
1
+ import {
2
+ getMessaging
3
+ } from "../chunk-AE55BKYV.mjs";
4
+ import "../chunk-WMJKH4XE.mjs";
5
+
6
+ // src/push/usePushNotifications.ts
7
+ import { useEffect, useRef } from "react";
8
+ import { Platform as Platform2 } from "react-native";
9
+ import * as Notifications2 from "expo-notifications";
10
+ import * as Device from "expo-device";
11
+ import { ada } from "@ateam-ai-mobile/core";
12
+ import { routePushTap, usePushBannerStore } from "@ateam-ai-mobile/ui";
13
+
14
+ // src/push/registration.ts
15
+ import { Platform } from "react-native";
16
+ import * as Notifications from "expo-notifications";
17
+ import { createPushRegistration, registerPushDevice, unregisterPushDevice } from "@ateam-ai-mobile/core";
18
+ var lifecycle = createPushRegistration({
19
+ // Adapt to the lifecycle's void contract — the notif-api returns a body it doesn't need.
20
+ registerDevice: async (token) => {
21
+ await registerPushDevice(token, Platform.OS === "ios" ? "ios" : "android");
22
+ },
23
+ unregisterDevice: async (token) => {
24
+ await unregisterPushDevice(token);
25
+ }
26
+ });
27
+ var pushRegistration = {
28
+ ...lifecycle,
29
+ async unregister() {
30
+ await lifecycle.unregister();
31
+ Promise.resolve().then(() => Notifications.dismissAllNotificationsAsync()).catch(
32
+ (e) => console.warn("[adas/push] clearing delivered notifications failed:", e?.message || e)
33
+ );
34
+ }
35
+ };
36
+
37
+ // src/push/usePushNotifications.ts
38
+ function usePushNotifications({ enabled, onOpenApp, foregroundHandler }) {
39
+ const onOpenAppRef = useRef(onOpenApp);
40
+ onOpenAppRef.current = onOpenApp;
41
+ useEffect(() => {
42
+ if (!enabled) return;
43
+ return startPushNotifications({ onOpenApp: () => onOpenAppRef.current?.(), foregroundHandler });
44
+ }, [enabled, foregroundHandler]);
45
+ }
46
+ var initialTapHandled = false;
47
+ var launchGeneration = pushRegistration.generation();
48
+ var handlerSet = false;
49
+ var activeStop = null;
50
+ function startPushNotifications(opts = {}) {
51
+ if (Platform2.OS === "web") return () => {
52
+ };
53
+ if (opts.foregroundHandler !== false) setForegroundHandler();
54
+ const messaging = getMessaging();
55
+ if (!messaging) return () => {
56
+ };
57
+ activeStop?.();
58
+ const startedIn = pushRegistration.generation();
59
+ let stopped = false;
60
+ const live = () => !stopped && pushRegistration.generation() === startedIn;
61
+ let unsubRefresh = null;
62
+ const routeTap = (msg) => {
63
+ if (live()) routePushTap(msg?.data, opts.onOpenApp);
64
+ };
65
+ (async () => {
66
+ const token = await registerForPush(messaging);
67
+ if (!token || !live()) return;
68
+ await pushRegistration.setToken(token);
69
+ if (!live()) return;
70
+ unsubRefresh = messaging().onTokenRefresh((next) => {
71
+ if (live()) void pushRegistration.setToken(next);
72
+ });
73
+ const initial = await messaging().getInitialNotification();
74
+ if (initial && !initialTapHandled && live() && startedIn === launchGeneration) {
75
+ initialTapHandled = true;
76
+ routeTap(initial);
77
+ }
78
+ })().catch((err) => console.warn("[adas/push] start failed:", err?.message || err));
79
+ const unsubMessage = messaging().onMessage((msg) => {
80
+ const n = msg.notification;
81
+ if (!live()) return;
82
+ if (!n?.title && !n?.body) return;
83
+ usePushBannerStore.getState().show({ title: n.title || "Ada", body: n.body || void 0, data: msg.data || {} });
84
+ });
85
+ const unsubOpened = messaging().onNotificationOpenedApp(routeTap);
86
+ const stop = () => {
87
+ if (stopped) return;
88
+ stopped = true;
89
+ unsubRefresh?.();
90
+ unsubMessage();
91
+ unsubOpened();
92
+ usePushBannerStore.getState().dismiss();
93
+ if (activeStop === stop) activeStop = null;
94
+ };
95
+ activeStop = stop;
96
+ return stop;
97
+ }
98
+ function setForegroundHandler() {
99
+ if (handlerSet) return;
100
+ handlerSet = true;
101
+ Notifications2.setNotificationHandler({
102
+ handleNotification: async () => ({
103
+ shouldShowAlert: true,
104
+ shouldPlaySound: true,
105
+ shouldSetBadge: true,
106
+ shouldShowBanner: true,
107
+ shouldShowList: true
108
+ })
109
+ });
110
+ }
111
+ async function registerForPush(messaging) {
112
+ if (!Device.isDevice) {
113
+ console.warn("[adas/push] requires a physical device");
114
+ return null;
115
+ }
116
+ try {
117
+ if (Platform2.OS === "ios") {
118
+ const status = await messaging().requestPermission();
119
+ const granted = status === messaging.AuthorizationStatus.AUTHORIZED || status === messaging.AuthorizationStatus.PROVISIONAL;
120
+ if (!granted) {
121
+ console.warn(`[adas/push] iOS permission not granted (status ${status})`);
122
+ return null;
123
+ }
124
+ } else if (Platform2.OS === "android") {
125
+ const { status: existing } = await Notifications2.getPermissionsAsync();
126
+ let final = existing;
127
+ if (existing !== "granted") {
128
+ const { status } = await Notifications2.requestPermissionsAsync();
129
+ final = status;
130
+ }
131
+ if (final !== "granted") {
132
+ console.warn(`[adas/push] Android permission not granted (${final})`);
133
+ return null;
134
+ }
135
+ await Notifications2.setNotificationChannelAsync("default", {
136
+ name: "Default",
137
+ importance: Notifications2.AndroidImportance.MAX,
138
+ vibrationPattern: [0, 250, 250, 250],
139
+ lightColor: ada.orange
140
+ });
141
+ }
142
+ if (Platform2.OS === "ios") {
143
+ if (!messaging().isDeviceRegisteredForRemoteMessages) {
144
+ await messaging().registerDeviceForRemoteMessages();
145
+ }
146
+ if (!await waitForApnsToken(messaging)) {
147
+ console.warn(
148
+ "[adas/push] APNs token never arrived \u2014 Firebase cannot mint an FCM token. Check the aps-environment entitlement and the APNs key in Firebase Console \u2192 Cloud Messaging \u2192 Apple app config."
149
+ );
150
+ }
151
+ }
152
+ const token = await messaging().getToken();
153
+ console.log(`[adas/push] FCM token ${token ? "acquired" : "missing"}`);
154
+ return token || null;
155
+ } catch (err) {
156
+ console.error("[adas/push] register failed:", err);
157
+ return null;
158
+ }
159
+ }
160
+ async function waitForApnsToken(messaging, maxMs = 5e3) {
161
+ const deadline = Date.now() + maxMs;
162
+ let token = await messaging().getAPNSToken();
163
+ while (!token && Date.now() < deadline) {
164
+ await new Promise((r) => setTimeout(r, 250));
165
+ token = await messaging().getAPNSToken();
166
+ }
167
+ return token || null;
168
+ }
169
+ export {
170
+ pushRegistration,
171
+ startPushNotifications,
172
+ usePushNotifications
173
+ };
174
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/push/usePushNotifications.ts","../../src/push/registration.ts"],"sourcesContent":["/**\n * Push notifications on this device — the headless half.\n *\n * startPushNotifications() (or the usePushNotifications hook around it) asks\n * for permission, gets this device's FCM token, registers it with Core through\n * pushRegistration, re-registers when Firebase refreshes it, and handles what\n * arrives:\n * - app open: a push with a visible notification goes to the in-app banner\n * (usePushBannerStore → <PushBanner/> from @ateam-ai-mobile/ui); data-only\n * pushes (silent syncs) are not shown;\n * - tapped in the background, or the tap that cold-started the app:\n * routePushTap (surface → prompt → the host's onOpenApp).\n * Run it only while signed in — registering needs the session token. Sign-out\n * unregisters through pushRegistration.unregister() in the host's teardown;\n * from that moment this start adopts no token and routes no tap, even before\n * the host's UI stops it. One push owner per app: a second start replaces the\n * first.\n *\n * Payload from Core's notif-router → FCM:\n * notification: { title, body }\n * data: { jobId, skillSlug, messageId, surfaceId?, pluginId?, prompt? }\n * In the background the OS shows the system banner itself.\n *\n * Moved from ateam-mobile's usePushNotifications (2026-10-29). Changed on the\n * way: the expo-notifications foreground handler is set on start, not at import\n * (an embedding app with its own passes foregroundHandler: false); a sign-out\n * while the token was being fetched or registered no longer registers the\n * signed-out phone or leaves a refresh listener behind; stopping clears the\n * in-app banner; data-only pushes no longer pop an empty banner; the tap that\n * opened the app is routed once, not again after every sign-in.\n */\nimport { useEffect, useRef } from 'react';\nimport { Platform } from 'react-native';\nimport * as Notifications from 'expo-notifications';\nimport * as Device from 'expo-device';\nimport { ada } from '@ateam-ai-mobile/core';\nimport { routePushTap, usePushBannerStore } from '@ateam-ai-mobile/ui';\nimport { getMessaging, type Messaging, type RemoteMessage } from './messaging';\nimport { pushRegistration } from './registration';\n\nexport interface PushNotificationsOptions {\n /** A tapped push that names no surface or prompt — e.g. show your chat screen. */\n onOpenApp?: () => void;\n /**\n * Set the expo-notifications handler so notifications also show while the app\n * is open (banner, list, sound, badge) — this covers the local notifications\n * plugins schedule. Default true; pass false if your app sets its own.\n */\n foregroundHandler?: boolean;\n}\n\nexport interface UsePushNotificationsOptions extends PushNotificationsOptions {\n /** Push runs while true — pass \"signed in\". */\n enabled: boolean;\n}\n\n/** Push while `enabled`; stops (listeners off) when it turns false or on unmount. */\nexport function usePushNotifications({ enabled, onOpenApp, foregroundHandler }: UsePushNotificationsOptions): void {\n const onOpenAppRef = useRef(onOpenApp);\n onOpenAppRef.current = onOpenApp;\n\n useEffect(() => {\n if (!enabled) return;\n return startPushNotifications({ onOpenApp: () => onOpenAppRef.current?.(), foregroundHandler });\n }, [enabled, foregroundHandler]);\n}\n\n// The tap that cold-started the app is routed once per launch — Firebase keeps\n// returning it, and a sign-out + sign-in starts push again — and never after a\n// sign-out: it was a push for the user signed in at launch.\nlet initialTapHandled = false;\nconst launchGeneration = pushRegistration.generation();\nlet handlerSet = false;\n// The running start's stop — one push owner per app.\nlet activeStop: (() => void) | null = null;\n\n/** Start push on this device; returns stop(). Call while signed in. No Firebase → no-op. */\nexport function startPushNotifications(opts: PushNotificationsOptions = {}): () => void {\n if (Platform.OS === 'web') return () => {};\n // Before the Firebase check: it also shows the local notifications plugins\n // schedule, which work without Firebase.\n if (opts.foregroundHandler !== false) setForegroundHandler();\n const messaging = getMessaging();\n if (!messaging) return () => {};\n activeStop?.();\n\n // Live = not stopped AND no sign-out since this start began. The host's\n // sign-out unregisters first and stops us only when its UI catches up; a\n // token or tap arriving in between belongs to the user who left.\n const startedIn = pushRegistration.generation();\n let stopped = false;\n const live = () => !stopped && pushRegistration.generation() === startedIn;\n let unsubRefresh: (() => void) | null = null;\n const routeTap = (msg: RemoteMessage | null) => {\n if (live()) routePushTap(msg?.data, opts.onOpenApp);\n };\n\n (async () => {\n const token = await registerForPush(messaging);\n if (!token || !live()) return;\n await pushRegistration.setToken(token);\n // Signed out while registering: the teardown already unregistered this\n // token — don't listen for refreshes as a signed-out user.\n if (!live()) return;\n unsubRefresh = messaging().onTokenRefresh((next) => {\n if (live()) void pushRegistration.setToken(next);\n });\n\n const initial = await messaging().getInitialNotification();\n if (initial && !initialTapHandled && live() && startedIn === launchGeneration) {\n initialTapHandled = true;\n routeTap(initial);\n }\n })().catch((err) => console.warn('[adas/push] start failed:', err?.message || err));\n\n // App open: Firebase delivers but the OS shows nothing — use the in-app banner.\n const unsubMessage = messaging().onMessage((msg) => {\n const n = msg.notification;\n if (!live()) return;\n if (!n?.title && !n?.body) return; // data-only (e.g. a silent sync) — nothing to show\n usePushBannerStore.getState().show({ title: n.title || 'Ada', body: n.body || undefined, data: msg.data || {} });\n });\n\n // App brought back from the background by a tap.\n const unsubOpened = messaging().onNotificationOpenedApp(routeTap);\n\n const stop = () => {\n if (stopped) return;\n stopped = true;\n unsubRefresh?.();\n unsubMessage();\n unsubOpened();\n // A banner shown for the user who is leaving must not stay tappable.\n usePushBannerStore.getState().dismiss();\n if (activeStop === stop) activeStop = null;\n };\n activeStop = stop;\n return stop;\n}\n\nfunction setForegroundHandler(): void {\n if (handlerSet) return;\n handlerSet = true;\n Notifications.setNotificationHandler({\n handleNotification: async () => ({\n shouldShowAlert: true,\n shouldPlaySound: true,\n shouldSetBadge: true,\n shouldShowBanner: true,\n shouldShowList: true,\n }),\n });\n}\n\nasync function registerForPush(messaging: Messaging): Promise<string | null> {\n if (!Device.isDevice) {\n console.warn('[adas/push] requires a physical device');\n return null;\n }\n\n try {\n if (Platform.OS === 'ios') {\n const status = await messaging().requestPermission();\n const granted =\n status === messaging.AuthorizationStatus.AUTHORIZED ||\n status === messaging.AuthorizationStatus.PROVISIONAL;\n if (!granted) {\n console.warn(`[adas/push] iOS permission not granted (status ${status})`);\n return null;\n }\n } else if (Platform.OS === 'android') {\n const { status: existing } = await Notifications.getPermissionsAsync();\n let final = existing;\n if (existing !== 'granted') {\n const { status } = await Notifications.requestPermissionsAsync();\n final = status;\n }\n if (final !== 'granted') {\n console.warn(`[adas/push] Android permission not granted (${final})`);\n return null;\n }\n await Notifications.setNotificationChannelAsync('default', {\n name: 'Default',\n importance: Notifications.AndroidImportance.MAX,\n vibrationPattern: [0, 250, 250, 250],\n lightColor: ada.orange,\n });\n }\n\n // iOS: register with APNs and WAIT for the APNs token before asking\n // Firebase for an FCM token. Calling getToken() before the APNs token\n // is available can return a stale/non-bridged token that the server\n // can't deliver to.\n if (Platform.OS === 'ios') {\n if (!messaging().isDeviceRegisteredForRemoteMessages) {\n await messaging().registerDeviceForRemoteMessages();\n }\n if (!(await waitForApnsToken(messaging))) {\n console.warn(\n '[adas/push] APNs token never arrived — Firebase cannot mint an FCM token. Check the aps-environment entitlement and the APNs key in Firebase Console → Cloud Messaging → Apple app config.',\n );\n }\n }\n\n const token = await messaging().getToken();\n console.log(`[adas/push] FCM token ${token ? 'acquired' : 'missing'}`);\n return token || null;\n } catch (err) {\n console.error('[adas/push] register failed:', err);\n return null;\n }\n}\n\n/** Polls getAPNSToken() up to ~5s. iOS sometimes needs a tick after\n * registerDeviceForRemoteMessages() returns before the token is available. */\nasync function waitForApnsToken(messaging: Messaging, maxMs = 5000): Promise<string | null> {\n const deadline = Date.now() + maxMs;\n let token = await messaging().getAPNSToken();\n while (!token && Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, 250));\n token = await messaging().getAPNSToken();\n }\n return token || null;\n}\n","/**\n * This device's push registration with Core — the one owner of the current\n * push token. The lifecycle (register, re-register on refresh, unregister on\n * sign-out, the sign-out count) is core's createPushRegistration; this wires it\n * to Core's notif-api with the platform. usePushNotifications feeds it tokens;\n * the host's sign-out teardown calls pushRegistration.unregister() while the\n * session token still exists. getToken() answers \"is push registered on this\n * device?\".\n */\nimport { Platform } from 'react-native';\nimport * as Notifications from 'expo-notifications';\nimport { createPushRegistration, registerPushDevice, unregisterPushDevice, type PushRegistration } from '@ateam-ai-mobile/core';\n\nconst lifecycle = createPushRegistration({\n // Adapt to the lifecycle's void contract — the notif-api returns a body it doesn't need.\n registerDevice: async (token) => { await registerPushDevice(token, Platform.OS === 'ios' ? 'ios' : 'android'); },\n unregisterDevice: async (token) => { await unregisterPushDevice(token); },\n});\n\nexport const pushRegistration: PushRegistration = {\n ...lifecycle,\n async unregister() {\n await lifecycle.unregister();\n // Notifications already in the list were for the user who is leaving, and\n // a push carries no recipient: tapped later, one would act in the next\n // user's chat. Best-effort, never blocks sign-out.\n Promise.resolve().then(() => Notifications.dismissAllNotificationsAsync()).catch((e: any) =>\n console.warn('[adas/push] clearing delivered notifications failed:', e?.message || e),\n );\n },\n};\n"],"mappings":"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","names":["Platform","Notifications","Platform"]}
@@ -0,0 +1,8 @@
1
+ interface SilentPushSyncOptions {
2
+ /** The saved session token — the same loader your auth session's persistence uses. */
3
+ loadToken: () => Promise<string | null>;
4
+ }
5
+ /** Register the background handler. No Firebase in this build → nothing to do. */
6
+ declare function registerSilentPushSync(opts: SilentPushSyncOptions): void;
7
+
8
+ export { type SilentPushSyncOptions, registerSilentPushSync };
@@ -0,0 +1,8 @@
1
+ interface SilentPushSyncOptions {
2
+ /** The saved session token — the same loader your auth session's persistence uses. */
3
+ loadToken: () => Promise<string | null>;
4
+ }
5
+ /** Register the background handler. No Firebase in this build → nothing to do. */
6
+ declare function registerSilentPushSync(opts: SilentPushSyncOptions): void;
7
+
8
+ export { type SilentPushSyncOptions, registerSilentPushSync };