@symbiote-native/notifications 0.1.0

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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/build/angular/index.d.ts +1 -0
  4. package/build/angular/index.js +1 -0
  5. package/build/core/background-task.d.ts +9 -0
  6. package/build/core/background-task.js +28 -0
  7. package/build/core/badge.d.ts +7 -0
  8. package/build/core/badge.js +23 -0
  9. package/build/core/categories.d.ts +10 -0
  10. package/build/core/categories.js +28 -0
  11. package/build/core/channels.d.ts +20 -0
  12. package/build/core/channels.js +106 -0
  13. package/build/core/emitter.d.ts +24 -0
  14. package/build/core/emitter.js +58 -0
  15. package/build/core/handler.d.ts +15 -0
  16. package/build/core/handler.js +50 -0
  17. package/build/core/index.d.ts +12 -0
  18. package/build/core/index.js +11 -0
  19. package/build/core/native-modules.d.ts +82 -0
  20. package/build/core/native-modules.js +33 -0
  21. package/build/core/permissions.d.ts +8 -0
  22. package/build/core/permissions.js +30 -0
  23. package/build/core/presenter.d.ts +7 -0
  24. package/build/core/presenter.js +27 -0
  25. package/build/core/scheduler.d.ts +13 -0
  26. package/build/core/scheduler.js +193 -0
  27. package/build/core/tokens.d.ts +30 -0
  28. package/build/core/tokens.js +192 -0
  29. package/build/core/types.d.ts +633 -0
  30. package/build/core/types.js +116 -0
  31. package/build/core/utils.d.ts +6 -0
  32. package/build/core/utils.js +53 -0
  33. package/build-ngc/angular/index.d.ts +1 -0
  34. package/build-ngc/angular/index.js +2 -0
  35. package/build-ngc/angular/index.js.map +1 -0
  36. package/build-ngc/core/background-task.d.ts +9 -0
  37. package/build-ngc/core/background-task.js +29 -0
  38. package/build-ngc/core/background-task.js.map +1 -0
  39. package/build-ngc/core/badge.d.ts +7 -0
  40. package/build-ngc/core/badge.js +24 -0
  41. package/build-ngc/core/badge.js.map +1 -0
  42. package/build-ngc/core/categories.d.ts +10 -0
  43. package/build-ngc/core/categories.js +29 -0
  44. package/build-ngc/core/categories.js.map +1 -0
  45. package/build-ngc/core/channels.d.ts +20 -0
  46. package/build-ngc/core/channels.js +107 -0
  47. package/build-ngc/core/channels.js.map +1 -0
  48. package/build-ngc/core/emitter.d.ts +24 -0
  49. package/build-ngc/core/emitter.js +59 -0
  50. package/build-ngc/core/emitter.js.map +1 -0
  51. package/build-ngc/core/handler.d.ts +15 -0
  52. package/build-ngc/core/handler.js +51 -0
  53. package/build-ngc/core/handler.js.map +1 -0
  54. package/build-ngc/core/index.d.ts +12 -0
  55. package/build-ngc/core/index.js +12 -0
  56. package/build-ngc/core/index.js.map +1 -0
  57. package/build-ngc/core/native-modules.d.ts +82 -0
  58. package/build-ngc/core/native-modules.js +34 -0
  59. package/build-ngc/core/native-modules.js.map +1 -0
  60. package/build-ngc/core/permissions.d.ts +8 -0
  61. package/build-ngc/core/permissions.js +31 -0
  62. package/build-ngc/core/permissions.js.map +1 -0
  63. package/build-ngc/core/presenter.d.ts +7 -0
  64. package/build-ngc/core/presenter.js +28 -0
  65. package/build-ngc/core/presenter.js.map +1 -0
  66. package/build-ngc/core/scheduler.d.ts +13 -0
  67. package/build-ngc/core/scheduler.js +194 -0
  68. package/build-ngc/core/scheduler.js.map +1 -0
  69. package/build-ngc/core/tokens.d.ts +30 -0
  70. package/build-ngc/core/tokens.js +193 -0
  71. package/build-ngc/core/tokens.js.map +1 -0
  72. package/build-ngc/core/types.d.ts +633 -0
  73. package/build-ngc/core/types.js +117 -0
  74. package/build-ngc/core/types.js.map +1 -0
  75. package/build-ngc/core/utils.d.ts +6 -0
  76. package/build-ngc/core/utils.js +54 -0
  77. package/build-ngc/core/utils.js.map +1 -0
  78. package/native-link.json +88 -0
  79. package/package.json +150 -0
  80. package/src/angular/index.ts +1 -0
  81. package/src/core/background-task.ts +32 -0
  82. package/src/core/badge.ts +26 -0
  83. package/src/core/categories.ts +60 -0
  84. package/src/core/channels.ts +183 -0
  85. package/src/core/emitter.ts +95 -0
  86. package/src/core/handler.ts +95 -0
  87. package/src/core/index.ts +144 -0
  88. package/src/core/native-modules.ts +209 -0
  89. package/src/core/permissions.ts +46 -0
  90. package/src/core/presenter.ts +45 -0
  91. package/src/core/scheduler.ts +265 -0
  92. package/src/core/tokens.ts +272 -0
  93. package/src/core/types.ts +767 -0
  94. package/src/core/utils.ts +82 -0
@@ -0,0 +1,192 @@
1
+ // Ported from expo-notifications @ sdk-57's getDevicePushTokenAsync.ts, getExpoPushTokenAsync.ts,
2
+ // unregisterForNotificationsAsync.ts, topicSubscription.ts, TokenEmitter.ts, and the
3
+ // `setAutoServerRegistrationEnabledAsync` half of DevicePushTokenAutoRegistration.fx.ts.
4
+ //
5
+ // ponytail: upstream's DevicePushTokenAutoRegistration.fx.ts also wires a permanent, module-load
6
+ // background daemon that re-POSTs the device token to Expo's push backend on every token change
7
+ // (with exponential backoff, via `abort-controller`) whenever registration was left enabled.
8
+ // That auto-resync daemon is NOT ported — an app that wants it can call
9
+ // `getExpoPushTokenAsync()` again from its own `addPushTokenListener` callback, which is the
10
+ // same effect without a permanent background retry loop and its extra dependency.
11
+ import { CodedError, Platform, UnavailabilityError, } from 'expo-modules-core';
12
+ import { dlog } from '@symbiote-native/engine';
13
+ import { pushTokenManager, serverRegistrationModule, topicSubscriptionModule, } from './native-modules.js';
14
+ const PRODUCTION_BASE_URL = 'https://exp.host/--/api/v2/';
15
+ // This package targets iOS/Android only (root CLAUDE.md); narrow `Platform.OS` at the one
16
+ // boundary that needs it rather than casting at every call site.
17
+ function nativePlatform() {
18
+ if (Platform.OS === 'ios' || Platform.OS === 'android') {
19
+ return Platform.OS;
20
+ }
21
+ throw new Error(`[notifications] unsupported platform: ${Platform.OS}`);
22
+ }
23
+ /** Fires whenever the OS rolls the device push token while the app is running. */
24
+ export function addPushTokenListener(listener) {
25
+ return pushTokenManager.addListener('onDevicePushToken', ({ devicePushToken }) => {
26
+ listener({ data: devicePushToken, type: nativePlatform() });
27
+ });
28
+ }
29
+ let nativeTokenPromise = null;
30
+ /** Returns the native FCM (Android) or APNs (iOS) device push token. */
31
+ export async function getDevicePushTokenAsync() {
32
+ if (!pushTokenManager.getDevicePushTokenAsync) {
33
+ throw new UnavailabilityError('Notifications', 'getDevicePushTokenAsync');
34
+ }
35
+ let devicePushToken;
36
+ if (nativeTokenPromise) {
37
+ devicePushToken = await nativeTokenPromise;
38
+ }
39
+ else {
40
+ nativeTokenPromise = pushTokenManager.getDevicePushTokenAsync();
41
+ devicePushToken = await nativeTokenPromise;
42
+ nativeTokenPromise = null;
43
+ }
44
+ dlog('[notifications] getDevicePushTokenAsync resolved');
45
+ return { type: nativePlatform(), data: devicePushToken };
46
+ }
47
+ /** Unregisters the device from receiving remote push notifications entirely. */
48
+ export async function unregisterForNotificationsAsync() {
49
+ if (!pushTokenManager.unregisterForNotificationsAsync) {
50
+ throw new UnavailabilityError('Notifications', 'unregisterForNotificationsAsync');
51
+ }
52
+ return pushTokenManager.unregisterForNotificationsAsync();
53
+ }
54
+ /** Subscribes the device to an FCM topic. @platform android */
55
+ export async function subscribeToTopicAsync(topic) {
56
+ if (!topicSubscriptionModule.subscribeToTopicAsync) {
57
+ throw new UnavailabilityError('Notifications', 'subscribeToTopicAsync');
58
+ }
59
+ return topicSubscriptionModule.subscribeToTopicAsync(topic);
60
+ }
61
+ /** Unsubscribes the device from an FCM topic. @platform android */
62
+ export async function unsubscribeFromTopicAsync(topic) {
63
+ if (!topicSubscriptionModule.unsubscribeFromTopicAsync) {
64
+ throw new UnavailabilityError('Notifications', 'unsubscribeFromTopicAsync');
65
+ }
66
+ return topicSubscriptionModule.unsubscribeFromTopicAsync(topic);
67
+ }
68
+ /**
69
+ * Enables/disables letting the OS-level registration blob persist whether the device push
70
+ * token should keep being pushed to the Expo push service backend. See the module doc comment
71
+ * above for what this deliberately does *not* do (the auto-resync daemon).
72
+ */
73
+ export async function setAutoServerRegistrationEnabledAsync(enabled) {
74
+ if (!serverRegistrationModule.setRegistrationInfoAsync) {
75
+ throw new UnavailabilityError('Notifications', 'setRegistrationInfoAsync');
76
+ }
77
+ if (!enabled) {
78
+ await serverRegistrationModule.setRegistrationInfoAsync(null);
79
+ return;
80
+ }
81
+ let existing = {};
82
+ try {
83
+ const info = await serverRegistrationModule.getRegistrationInfoAsync?.();
84
+ if (info) {
85
+ existing = JSON.parse(info);
86
+ }
87
+ }
88
+ catch {
89
+ // Corrupt/missing persisted blob — start fresh.
90
+ }
91
+ existing.isEnabled = true;
92
+ await serverRegistrationModule.setRegistrationInfoAsync(JSON.stringify(existing));
93
+ }
94
+ async function getDeviceIdAsync() {
95
+ try {
96
+ if (!serverRegistrationModule.getInstallationIdAsync) {
97
+ throw new UnavailabilityError('Notifications', 'getInstallationIdAsync');
98
+ }
99
+ return await serverRegistrationModule.getInstallationIdAsync();
100
+ }
101
+ catch (error) {
102
+ throw new CodedError('ERR_NOTIF_DEVICE_ID', `Could not fetch the installation ID of the application: ${error}.`);
103
+ }
104
+ }
105
+ function getTypeOfToken(devicePushToken) {
106
+ switch (devicePushToken.type) {
107
+ case 'ios':
108
+ return 'apns';
109
+ case 'android':
110
+ return 'fcm';
111
+ }
112
+ }
113
+ function getDeviceToken(devicePushToken) {
114
+ return devicePushToken.data;
115
+ }
116
+ async function parseJsonResponse(response) {
117
+ try {
118
+ return await response.json();
119
+ }
120
+ catch {
121
+ throw new CodedError('ERR_NOTIFICATIONS_SERVER_ERROR', 'Expected a JSON response from server when fetching Expo token.');
122
+ }
123
+ }
124
+ function extractExpoPushToken(data) {
125
+ if (data !== null &&
126
+ typeof data === 'object' &&
127
+ 'data' in data &&
128
+ data.data !== null &&
129
+ typeof data.data === 'object' &&
130
+ 'expoPushToken' in data.data &&
131
+ typeof data.data.expoPushToken === 'string') {
132
+ return data.data.expoPushToken;
133
+ }
134
+ throw new CodedError('ERR_NOTIFICATIONS_SERVER_ERROR', `Malformed response from server, expected "{ data: { expoPushToken: string } }", received: ${JSON.stringify(data)}.`);
135
+ }
136
+ /**
137
+ * Returns an Expo push token usable with Expo's push notification relay service. Makes a
138
+ * network request to Expo's servers — wrap in try/catch and retry once the device is back
139
+ * online.
140
+ *
141
+ * ponytail: upstream auto-fills `projectId`/`applicationId`/`development` from
142
+ * `expo-constants`/`expo-application`. Neither package is part of this port, so pass them
143
+ * explicitly (`applicationId` from `@symbiote-native/application`'s own `applicationId` export
144
+ * covers the second one).
145
+ */
146
+ export async function getExpoPushTokenAsync(options = {}) {
147
+ const devicePushToken = options.devicePushToken ?? (await getDevicePushTokenAsync());
148
+ const deviceId = options.deviceId ?? (await getDeviceIdAsync());
149
+ if (!options.projectId) {
150
+ throw new CodedError('ERR_NOTIFICATIONS_NO_EXPERIENCE_ID', 'No "projectId" was passed to getExpoPushTokenAsync(). This port does not read expo-constants — pass it explicitly.');
151
+ }
152
+ if (!options.applicationId) {
153
+ throw new CodedError('ERR_NOTIFICATIONS_NO_APPLICATION_ID', 'No "applicationId" was passed to getExpoPushTokenAsync(). This port does not read expo-application — pass it explicitly (e.g. @symbiote-native/application\'s `applicationId`).');
154
+ }
155
+ const type = options.type ?? getTypeOfToken(devicePushToken);
156
+ const development = options.development ?? false;
157
+ const baseUrl = options.baseUrl ?? PRODUCTION_BASE_URL;
158
+ const url = options.url ?? `${baseUrl}push/getExpoPushToken`;
159
+ const body = {
160
+ type,
161
+ deviceId: deviceId.toLowerCase(),
162
+ development,
163
+ appId: options.applicationId,
164
+ deviceToken: getDeviceToken(devicePushToken),
165
+ projectId: options.projectId,
166
+ };
167
+ let response;
168
+ try {
169
+ response = await fetch(url, {
170
+ method: 'POST',
171
+ headers: { 'content-type': 'application/json' },
172
+ body: JSON.stringify(body),
173
+ });
174
+ }
175
+ catch (error) {
176
+ throw new CodedError('ERR_NOTIFICATIONS_NETWORK_ERROR', `Error encountered while fetching Expo token: ${error}.`);
177
+ }
178
+ if (!response.ok) {
179
+ throw new CodedError('ERR_NOTIFICATIONS_SERVER_ERROR', `Error encountered while fetching Expo token, expected an OK response, received: ${response.status}.`);
180
+ }
181
+ const expoPushToken = extractExpoPushToken(await parseJsonResponse(response));
182
+ if (!options.url && !options.baseUrl) {
183
+ try {
184
+ await setAutoServerRegistrationEnabledAsync(true);
185
+ }
186
+ catch (error) {
187
+ console.warn('[notifications] Could not enable auto server registration', error);
188
+ }
189
+ }
190
+ dlog('[notifications] getExpoPushTokenAsync resolved');
191
+ return { type: 'expo', data: expoPushToken };
192
+ }