@symbiote-native/notifications 0.1.1 → 0.1.3

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 (100) hide show
  1. package/README.md +94 -58
  2. package/build/angular/index.d.ts +1 -0
  3. package/build/angular/index.js +1 -0
  4. package/build/angular/services/last-notification-response.service/index.d.ts +8 -0
  5. package/build/angular/services/last-notification-response.service/index.js +78 -0
  6. package/build/core/auto-registration.d.ts +5 -0
  7. package/build/core/auto-registration.js +65 -0
  8. package/build/core/backoff.d.ts +6 -0
  9. package/build/core/backoff.js +15 -0
  10. package/build/core/device-token.d.ts +7 -0
  11. package/build/core/device-token.js +35 -0
  12. package/build/core/emitter.d.ts +4 -0
  13. package/build/core/emitter.js +8 -0
  14. package/build/core/index.d.ts +3 -1
  15. package/build/core/index.js +3 -1
  16. package/build/core/last-notification-response.d.ts +2 -0
  17. package/build/core/last-notification-response.js +12 -0
  18. package/build/core/registration-abort.d.ts +2 -0
  19. package/build/core/registration-abort.js +10 -0
  20. package/build/core/token-metadata.d.ts +4 -0
  21. package/build/core/token-metadata.js +34 -0
  22. package/build/core/tokens.d.ts +4 -22
  23. package/build/core/tokens.js +41 -99
  24. package/build/core/update-device-push-token.d.ts +3 -0
  25. package/build/core/update-device-push-token.js +122 -0
  26. package/build/react/hooks/use-last-notification-response/index.d.ts +2 -0
  27. package/build/react/hooks/use-last-notification-response/index.js +22 -0
  28. package/build/react/index.d.ts +2 -0
  29. package/build/react/index.js +2 -0
  30. package/build/solid/index.d.ts +2 -0
  31. package/build/solid/index.js +2 -0
  32. package/build/solid/primitives/create-last-notification-response.d.ts +3 -0
  33. package/build/solid/primitives/create-last-notification-response.js +20 -0
  34. package/build/svelte/index.d.ts +2 -0
  35. package/build/svelte/index.js +2 -0
  36. package/build/svelte/use-last-notification-response.svelte.d.ts +4 -0
  37. package/build/svelte/use-last-notification-response.svelte.js +25 -0
  38. package/build/vue/composables/use-last-notification-response/index.d.ts +3 -0
  39. package/build/vue/composables/use-last-notification-response/index.js +20 -0
  40. package/build/vue/index.d.ts +2 -0
  41. package/build/vue/index.js +2 -0
  42. package/build-ngc/angular/index.d.ts +1 -0
  43. package/build-ngc/angular/index.js +1 -0
  44. package/build-ngc/angular/index.js.map +1 -1
  45. package/build-ngc/angular/services/last-notification-response.service/index.d.ts +11 -0
  46. package/build-ngc/angular/services/last-notification-response.service/index.js +37 -0
  47. package/build-ngc/angular/services/last-notification-response.service/index.js.map +1 -0
  48. package/build-ngc/core/auto-registration.d.ts +5 -0
  49. package/build-ngc/core/auto-registration.js +66 -0
  50. package/build-ngc/core/auto-registration.js.map +1 -0
  51. package/build-ngc/core/backoff.d.ts +6 -0
  52. package/build-ngc/core/backoff.js +16 -0
  53. package/build-ngc/core/backoff.js.map +1 -0
  54. package/build-ngc/core/device-token.d.ts +7 -0
  55. package/build-ngc/core/device-token.js +36 -0
  56. package/build-ngc/core/device-token.js.map +1 -0
  57. package/build-ngc/core/emitter.d.ts +4 -0
  58. package/build-ngc/core/emitter.js +8 -0
  59. package/build-ngc/core/emitter.js.map +1 -1
  60. package/build-ngc/core/index.d.ts +3 -1
  61. package/build-ngc/core/index.js +3 -1
  62. package/build-ngc/core/index.js.map +1 -1
  63. package/build-ngc/core/last-notification-response.d.ts +2 -0
  64. package/build-ngc/core/last-notification-response.js +13 -0
  65. package/build-ngc/core/last-notification-response.js.map +1 -0
  66. package/build-ngc/core/registration-abort.d.ts +2 -0
  67. package/build-ngc/core/registration-abort.js +11 -0
  68. package/build-ngc/core/registration-abort.js.map +1 -0
  69. package/build-ngc/core/token-metadata.d.ts +4 -0
  70. package/build-ngc/core/token-metadata.js +35 -0
  71. package/build-ngc/core/token-metadata.js.map +1 -0
  72. package/build-ngc/core/tokens.d.ts +4 -22
  73. package/build-ngc/core/tokens.js +41 -99
  74. package/build-ngc/core/tokens.js.map +1 -1
  75. package/build-ngc/core/update-device-push-token.d.ts +3 -0
  76. package/build-ngc/core/update-device-push-token.js +123 -0
  77. package/build-ngc/core/update-device-push-token.js.map +1 -0
  78. package/package.json +24 -23
  79. package/src/angular/index.ts +1 -0
  80. package/src/angular/services/last-notification-response.service/index.ts +54 -0
  81. package/src/core/auto-registration.ts +105 -0
  82. package/src/core/backoff.ts +24 -0
  83. package/src/core/device-token.ts +54 -0
  84. package/src/core/emitter.ts +10 -0
  85. package/src/core/index.ts +8 -0
  86. package/src/core/last-notification-response.ts +19 -0
  87. package/src/core/registration-abort.ts +12 -0
  88. package/src/core/token-metadata.ts +42 -0
  89. package/src/core/tokens.ts +71 -133
  90. package/src/core/update-device-push-token.ts +173 -0
  91. package/src/react/hooks/use-last-notification-response/index.ts +35 -0
  92. package/src/react/index.ts +2 -0
  93. package/src/solid/index.ts +2 -0
  94. package/src/solid/primitives/create-last-notification-response.ts +35 -0
  95. package/src/svelte/index.ts +2 -0
  96. package/src/svelte/svelte-ambient.d.ts +3 -0
  97. package/src/svelte/svelte-compile.test-helper.ts +7 -0
  98. package/src/svelte/use-last-notification-response.svelte.ts +45 -0
  99. package/src/vue/composables/use-last-notification-response/index.ts +35 -0
  100. package/src/vue/index.ts +2 -0
@@ -0,0 +1,34 @@
1
+ import { getIosPushNotificationServiceEnvironmentAsync } from '@symbiote-native/application';
2
+ import { CodedError, Platform, UnavailabilityError } from 'expo-modules-core';
3
+ import { serverRegistrationModule } from './native-modules.js';
4
+ export async function getDeviceIdAsync() {
5
+ try {
6
+ if (!serverRegistrationModule.getInstallationIdAsync) {
7
+ throw new UnavailabilityError('Notifications', 'getInstallationIdAsync');
8
+ }
9
+ return await serverRegistrationModule.getInstallationIdAsync();
10
+ }
11
+ catch (error) {
12
+ throw new CodedError('ERR_NOTIF_DEVICE_ID', `Could not fetch the installation ID of the application: ${error}.`);
13
+ }
14
+ }
15
+ export function getTypeOfToken(devicePushToken) {
16
+ switch (devicePushToken.type) {
17
+ case 'ios':
18
+ return 'apns';
19
+ case 'android':
20
+ return 'fcm';
21
+ }
22
+ }
23
+ export async function shouldUseDevelopmentNotificationService() {
24
+ if (Platform.OS === 'ios') {
25
+ try {
26
+ return ((await getIosPushNotificationServiceEnvironmentAsync()) ===
27
+ 'development');
28
+ }
29
+ catch {
30
+ // Не смогли узнать окружение, считаем что не development
31
+ }
32
+ }
33
+ return false;
34
+ }
@@ -1,30 +1,12 @@
1
- import { type EventSubscription } from 'expo-modules-core';
2
- import type { IDevicePushToken, IExpoPushToken, IExpoPushTokenOptions } from './types';
3
- export type IPushTokenListener = (token: IDevicePushToken) => void;
4
- /** Fires whenever the OS rolls the device push token while the app is running. */
5
- export declare function addPushTokenListener(listener: IPushTokenListener): EventSubscription;
6
- /** Returns the native FCM (Android) or APNs (iOS) device push token. */
7
- export declare function getDevicePushTokenAsync(): Promise<IDevicePushToken>;
1
+ import type { IExpoPushToken, IExpoPushTokenOptions } from './types';
2
+ export { addPushTokenListener, getDevicePushTokenAsync, type IPushTokenListener, } from './device-token';
8
3
  /** Unregisters the device from receiving remote push notifications entirely. */
9
4
  export declare function unregisterForNotificationsAsync(): Promise<void>;
10
5
  /** Subscribes the device to an FCM topic. @platform android */
11
6
  export declare function subscribeToTopicAsync(topic: string): Promise<null>;
12
7
  /** Unsubscribes the device from an FCM topic. @platform android */
13
8
  export declare function unsubscribeFromTopicAsync(topic: string): Promise<null>;
14
- /**
15
- * Enables/disables letting the OS-level registration blob persist whether the device push
16
- * token should keep being pushed to the Expo push service backend. See the module doc comment
17
- * above for what this deliberately does *not* do (the auto-resync daemon).
18
- */
9
+ /** Persists whether the device push token keeps being pushed to the Expo push service */
19
10
  export declare function setAutoServerRegistrationEnabledAsync(enabled: boolean): Promise<void>;
20
- /**
21
- * Returns an Expo push token usable with Expo's push notification relay service. Makes a
22
- * network request to Expo's servers — wrap in try/catch and retry once the device is back
23
- * online.
24
- *
25
- * ponytail: upstream auto-fills `projectId`/`applicationId`/`development` from
26
- * `expo-constants`/`expo-application`. Neither package is part of this port, so pass them
27
- * explicitly (`applicationId` from `@symbiote-native/application`'s own `applicationId` export
28
- * covers the second one).
29
- */
11
+ /** Returns an Expo push token for Expo's push relay, needs network so retry when back online */
30
12
  export declare function getExpoPushTokenAsync(options?: IExpoPushTokenOptions): Promise<IExpoPushToken>;
@@ -1,49 +1,14 @@
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';
1
+ // Порт токенов из upstream sdk-57, фоновый ресинк токена живёт в `auto-registration.ts`
2
+ import { applicationId } from '@symbiote-native/application';
3
+ import { CodedError, UnavailabilityError } from 'expo-modules-core';
12
4
  import { dlog } from '@symbiote-native/engine';
5
+ import { installPushTokenAutoRegistration } from './auto-registration.js';
6
+ import { getDevicePushTokenAsync } from './device-token.js';
13
7
  import { pushTokenManager, serverRegistrationModule, topicSubscriptionModule, } from './native-modules.js';
8
+ import { abortPendingRegistration } from './registration-abort.js';
9
+ import { getDeviceIdAsync, getTypeOfToken, shouldUseDevelopmentNotificationService, } from './token-metadata.js';
10
+ export { addPushTokenListener, getDevicePushTokenAsync, } from './device-token.js';
14
11
  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
12
  /** Unregisters the device from receiving remote push notifications entirely. */
48
13
  export async function unregisterForNotificationsAsync() {
49
14
  if (!pushTokenManager.unregisterForNotificationsAsync) {
@@ -65,12 +30,10 @@ export async function unsubscribeFromTopicAsync(topic) {
65
30
  }
66
31
  return topicSubscriptionModule.unsubscribeFromTopicAsync(topic);
67
32
  }
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
- */
33
+ /** Persists whether the device push token keeps being pushed to the Expo push service */
73
34
  export async function setAutoServerRegistrationEnabledAsync(enabled) {
35
+ // Перезаписываем регистрацию, поэтому висящий запрос не должен завершиться
36
+ abortPendingRegistration();
74
37
  if (!serverRegistrationModule.setRegistrationInfoAsync) {
75
38
  throw new UnavailabilityError('Notifications', 'setRegistrationInfoAsync');
76
39
  }
@@ -90,25 +53,7 @@ export async function setAutoServerRegistrationEnabledAsync(enabled) {
90
53
  }
91
54
  existing.isEnabled = true;
92
55
  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
- }
56
+ installPushTokenAutoRegistration();
112
57
  }
113
58
  function getDeviceToken(devicePushToken) {
114
59
  return devicePushToken.data;
@@ -133,37 +78,7 @@ function extractExpoPushToken(data) {
133
78
  }
134
79
  throw new CodedError('ERR_NOTIFICATIONS_SERVER_ERROR', `Malformed response from server, expected "{ data: { expoPushToken: string } }", received: ${JSON.stringify(data)}.`);
135
80
  }
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
- };
81
+ async function requestExpoPushTokenAsync(url, body) {
167
82
  let response;
168
83
  try {
169
84
  response = await fetch(url, {
@@ -178,7 +93,34 @@ export async function getExpoPushTokenAsync(options = {}) {
178
93
  if (!response.ok) {
179
94
  throw new CodedError('ERR_NOTIFICATIONS_SERVER_ERROR', `Error encountered while fetching Expo token, expected an OK response, received: ${response.status}.`);
180
95
  }
181
- const expoPushToken = extractExpoPushToken(await parseJsonResponse(response));
96
+ return extractExpoPushToken(await parseJsonResponse(response));
97
+ }
98
+ // Вынесено, чтобы `getExpoPushTokenAsync` читался как запрос токена и регистрация
99
+ async function buildExpoPushTokenBody(options, devicePushToken) {
100
+ if (!options.projectId) {
101
+ throw new CodedError('ERR_NOTIFICATIONS_NO_EXPERIENCE_ID', 'No "projectId" was passed to getExpoPushTokenAsync(). Pass it explicitly (this port has no expo-constants).');
102
+ }
103
+ const appId = options.applicationId ?? applicationId;
104
+ if (!appId) {
105
+ throw new CodedError('ERR_NOTIFICATIONS_NO_APPLICATION_ID', 'No "applicationId" was passed to getExpoPushTokenAsync() and the app has none of its own.');
106
+ }
107
+ const deviceId = options.deviceId ?? (await getDeviceIdAsync());
108
+ return {
109
+ type: options.type ?? getTypeOfToken(devicePushToken),
110
+ deviceId: deviceId.toLowerCase(),
111
+ development: options.development ?? (await shouldUseDevelopmentNotificationService()),
112
+ appId,
113
+ deviceToken: getDeviceToken(devicePushToken),
114
+ projectId: options.projectId,
115
+ };
116
+ }
117
+ /** Returns an Expo push token for Expo's push relay, needs network so retry when back online */
118
+ export async function getExpoPushTokenAsync(options = {}) {
119
+ const devicePushToken = options.devicePushToken ?? (await getDevicePushTokenAsync());
120
+ const body = await buildExpoPushTokenBody(options, devicePushToken);
121
+ const baseUrl = options.baseUrl ?? PRODUCTION_BASE_URL;
122
+ const url = options.url ?? `${baseUrl}push/getExpoPushToken`;
123
+ const expoPushToken = await requestExpoPushTokenAsync(url, body);
182
124
  if (!options.url && !options.baseUrl) {
183
125
  try {
184
126
  await setAutoServerRegistrationEnabledAsync(true);
@@ -0,0 +1,3 @@
1
+ import type { IDevicePushToken } from './types';
2
+ export declare function hasDeviceTokenChangedAsync(token: IDevicePushToken): Promise<boolean>;
3
+ export declare function updateDevicePushTokenAsync(signal: AbortSignal, token: IDevicePushToken): Promise<void>;
@@ -0,0 +1,122 @@
1
+ // Порт `utils/updateDevicePushTokenAsync.ts` из upstream sdk-57
2
+ import { applicationId } from '@symbiote-native/application';
3
+ import { dlog } from '@symbiote-native/engine';
4
+ import { computeNextBackoffInterval } from './backoff.js';
5
+ import { serverRegistrationModule } from './native-modules.js';
6
+ import { getDeviceIdAsync, getTypeOfToken, shouldUseDevelopmentNotificationService, } from './token-metadata.js';
7
+ const UPDATE_DEVICE_PUSH_TOKEN_URL = 'https://exp.host/--/api/v2/push/updateDeviceToken';
8
+ const LAST_TOKEN_KEY = 'lastRegisteredDeviceToken';
9
+ const INITIAL_BACKOFF_MS = 500;
10
+ const MAX_BACKOFF_MS = 2 * 60 * 1000;
11
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
12
+ // Повторная регистрация раз в 7 дней, т.к. сервер мог потерять запись об устройстве
13
+ const REGISTRATION_TTL_MS = 7 * MS_PER_DAY;
14
+ function isRecord(value) {
15
+ return typeof value === 'object' && value !== null;
16
+ }
17
+ function isStoredTokenData(value) {
18
+ return (isRecord(value) &&
19
+ typeof value.deviceToken === 'string' &&
20
+ typeof value.development === 'boolean' &&
21
+ typeof value.type === 'string' &&
22
+ typeof value.registeredAt === 'number');
23
+ }
24
+ async function getLastRegisteredTokenDataAsync() {
25
+ try {
26
+ if (!serverRegistrationModule.getRegistrationInfoAsync)
27
+ return null;
28
+ const info = await serverRegistrationModule.getRegistrationInfoAsync();
29
+ if (!info)
30
+ return null;
31
+ const stored = JSON.parse(info)?.[LAST_TOKEN_KEY];
32
+ return isStoredTokenData(stored) ? stored : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ async function setLastRegisteredTokenDataAsync(tokenData) {
39
+ try {
40
+ if (!serverRegistrationModule.getRegistrationInfoAsync ||
41
+ !serverRegistrationModule.setRegistrationInfoAsync) {
42
+ return;
43
+ }
44
+ const info = await serverRegistrationModule.getRegistrationInfoAsync();
45
+ const existing = info ? JSON.parse(info) : {};
46
+ existing[LAST_TOKEN_KEY] = tokenData;
47
+ await serverRegistrationModule.setRegistrationInfoAsync(JSON.stringify(existing));
48
+ }
49
+ catch {
50
+ // Best-effort, при следующем запуске приложение зарегистрируется снова
51
+ }
52
+ }
53
+ // Если проверка невозможна, считаем токен изменившимся (fail-open)
54
+ export async function hasDeviceTokenChangedAsync(token) {
55
+ try {
56
+ const development = await shouldUseDevelopmentNotificationService();
57
+ const lastTokenData = await getLastRegisteredTokenDataAsync();
58
+ if (lastTokenData == null)
59
+ return true;
60
+ const age = Date.now() - lastTokenData.registeredAt;
61
+ if (age < 0 || age >= REGISTRATION_TTL_MS)
62
+ return true;
63
+ return (token.data !== lastTokenData.deviceToken ||
64
+ applicationId !== lastTokenData.appId ||
65
+ development !== lastTokenData.development ||
66
+ getTypeOfToken(token) !== lastTokenData.type);
67
+ }
68
+ catch {
69
+ return true;
70
+ }
71
+ }
72
+ // `true` значит запрос надо повторить
73
+ async function sendTokenOnceAsync(signal, token) {
74
+ const [development, deviceId] = await Promise.all([
75
+ shouldUseDevelopmentNotificationService(),
76
+ getDeviceIdAsync(),
77
+ ]);
78
+ const type = getTypeOfToken(token);
79
+ try {
80
+ const response = await fetch(UPDATE_DEVICE_PUSH_TOKEN_URL, {
81
+ method: 'POST',
82
+ headers: { 'content-type': 'application/json' },
83
+ body: JSON.stringify({
84
+ deviceId: deviceId.toLowerCase(),
85
+ development,
86
+ deviceToken: token.data,
87
+ appId: applicationId,
88
+ type,
89
+ }),
90
+ signal,
91
+ });
92
+ if (!response.ok) {
93
+ dlog(`[notifications] device push token update rejected: ${await response.text()}`);
94
+ return true;
95
+ }
96
+ await setLastRegisteredTokenDataAsync({
97
+ deviceToken: token.data,
98
+ appId: applicationId,
99
+ development,
100
+ type,
101
+ registeredAt: Date.now(),
102
+ });
103
+ return false;
104
+ }
105
+ catch (error) {
106
+ // Прерванный запрос может упасть с `AbortError` или нативной ошибкой отмены
107
+ if (signal.aborted || (isRecord(error) && error.name === 'AbortError'))
108
+ return false;
109
+ console.warn('[notifications] Error thrown while updating the device push token with the server:', error);
110
+ return true;
111
+ }
112
+ }
113
+ export async function updateDevicePushTokenAsync(signal, token) {
114
+ const backoffOptions = { maxBackoff: MAX_BACKOFF_MS };
115
+ for (let retriesCount = 0; !signal.aborted; retriesCount += 1) {
116
+ const shouldRetry = await sendTokenOnceAsync(signal, token);
117
+ if (!shouldRetry || signal.aborted)
118
+ return;
119
+ const backoff = computeNextBackoffInterval(INITIAL_BACKOFF_MS, retriesCount, backoffOptions);
120
+ await new Promise(resolve => setTimeout(resolve, backoff));
121
+ }
122
+ }
@@ -0,0 +1,2 @@
1
+ import type { IMaybeNotificationResponse } from '../../../core';
2
+ export declare function useLastNotificationResponse(): IMaybeNotificationResponse;
@@ -0,0 +1,22 @@
1
+ // React twin of expo-notifications' `useLastNotificationResponse`. `useLayoutEffect` matches
2
+ // upstream's own choice - the listener must register before paint
3
+ import { useLayoutEffect, useState } from 'react';
4
+ import { addNotificationResponseClearedListener, addNotificationResponseReceivedListener, determineNextResponse, getLastNotificationResponse, } from '../../../core/index.js';
5
+ export function useLastNotificationResponse() {
6
+ const [lastResponse, setLastResponse] = useState(undefined);
7
+ useLayoutEffect(() => {
8
+ const response = getLastNotificationResponse();
9
+ setLastResponse(prev => determineNextResponse(prev, response));
10
+ const subscription = addNotificationResponseReceivedListener(next => {
11
+ setLastResponse(prev => determineNextResponse(prev, next));
12
+ });
13
+ const clearedSubscription = addNotificationResponseClearedListener(() => {
14
+ setLastResponse(null);
15
+ });
16
+ return () => {
17
+ subscription.remove();
18
+ clearedSubscription.remove();
19
+ };
20
+ }, []);
21
+ return lastResponse;
22
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { useLastNotificationResponse } from './hooks/use-last-notification-response';
@@ -0,0 +1,2 @@
1
+ export * from '../core/index.js';
2
+ export { useLastNotificationResponse } from './hooks/use-last-notification-response/index.js';
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { createLastNotificationResponse } from './primitives/create-last-notification-response';
@@ -0,0 +1,2 @@
1
+ export * from '../core/index.js';
2
+ export { createLastNotificationResponse } from './primitives/create-last-notification-response.js';
@@ -0,0 +1,3 @@
1
+ import { type Accessor } from 'solid-js';
2
+ import type { IMaybeNotificationResponse } from '../../core';
3
+ export declare function createLastNotificationResponse(): Accessor<IMaybeNotificationResponse>;
@@ -0,0 +1,20 @@
1
+ // Solid twin of `../../react`'s `useLastNotificationResponse`
2
+ import { createSignal, onCleanup, onMount } from 'solid-js';
3
+ import { addNotificationResponseClearedListener, addNotificationResponseReceivedListener, determineNextResponse, getLastNotificationResponse, } from '../../core/index.js';
4
+ export function createLastNotificationResponse() {
5
+ const [lastResponse, setLastResponse] = createSignal(undefined);
6
+ onMount(() => {
7
+ setLastResponse(prev => determineNextResponse(prev, getLastNotificationResponse()));
8
+ const subscription = addNotificationResponseReceivedListener(next => {
9
+ setLastResponse(prev => determineNextResponse(prev, next));
10
+ });
11
+ const clearedSubscription = addNotificationResponseClearedListener(() => {
12
+ setLastResponse(null);
13
+ });
14
+ onCleanup(() => {
15
+ subscription.remove();
16
+ clearedSubscription.remove();
17
+ });
18
+ });
19
+ return lastResponse;
20
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { useLastNotificationResponse } from './use-last-notification-response.svelte';
@@ -0,0 +1,2 @@
1
+ export * from '../core/index.js';
2
+ export { useLastNotificationResponse } from './use-last-notification-response.svelte';
@@ -0,0 +1,4 @@
1
+ import type { IMaybeNotificationResponse } from '../core';
2
+ export declare function useLastNotificationResponse(): {
3
+ readonly current: IMaybeNotificationResponse;
4
+ };
@@ -0,0 +1,25 @@
1
+ // Boxed-getter shape matches `@symbiote-native/screen-capture`'s own runes by design - each
2
+ // package owns its copy rather than depend on a sibling install-optional package
3
+ import { untrack } from 'svelte';
4
+ import { addNotificationResponseClearedListener, addNotificationResponseReceivedListener, determineNextResponse, getLastNotificationResponse, } from '../core/index.js';
5
+ export function useLastNotificationResponse() {
6
+ let lastResponse = $state(undefined);
7
+ $effect(() => {
8
+ lastResponse = determineNextResponse(untrack(() => lastResponse), getLastNotificationResponse());
9
+ const subscription = addNotificationResponseReceivedListener(next => {
10
+ lastResponse = determineNextResponse(untrack(() => lastResponse), next);
11
+ });
12
+ const clearedSubscription = addNotificationResponseClearedListener(() => {
13
+ lastResponse = null;
14
+ });
15
+ return () => {
16
+ subscription.remove();
17
+ clearedSubscription.remove();
18
+ };
19
+ });
20
+ return {
21
+ get current() {
22
+ return lastResponse;
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,3 @@
1
+ import { type Ref } from '@vue/runtime-core';
2
+ import type { IMaybeNotificationResponse } from '../../../core';
3
+ export declare function useLastNotificationResponse(): Ref<IMaybeNotificationResponse>;
@@ -0,0 +1,20 @@
1
+ // Vue twin of `../../../react`'s `useLastNotificationResponse`
2
+ import { onMounted, onUnmounted, ref } from '@vue/runtime-core';
3
+ import { addNotificationResponseClearedListener, addNotificationResponseReceivedListener, determineNextResponse, getLastNotificationResponse, } from '../../../core/index.js';
4
+ export function useLastNotificationResponse() {
5
+ const lastResponse = ref(undefined);
6
+ onMounted(() => {
7
+ lastResponse.value = determineNextResponse(lastResponse.value, getLastNotificationResponse());
8
+ const subscription = addNotificationResponseReceivedListener(next => {
9
+ lastResponse.value = determineNextResponse(lastResponse.value, next);
10
+ });
11
+ const clearedSubscription = addNotificationResponseClearedListener(() => {
12
+ lastResponse.value = null;
13
+ });
14
+ onUnmounted(() => {
15
+ subscription.remove();
16
+ clearedSubscription.remove();
17
+ });
18
+ });
19
+ return lastResponse;
20
+ }
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export { useLastNotificationResponse } from './composables/use-last-notification-response';
@@ -0,0 +1,2 @@
1
+ export * from '../core/index.js';
2
+ export { useLastNotificationResponse } from './composables/use-last-notification-response/index.js';
@@ -1 +1,2 @@
1
1
  export * from '../core';
2
+ export { LastNotificationResponseService } from './services/last-notification-response.service';
@@ -1,2 +1,3 @@
1
1
  export * from '../core';
2
+ export { LastNotificationResponseService } from './services/last-notification-response.service';
2
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,+BAA+B,EAAE,MAAM,+CAA+C,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { type Signal } from '@angular/core';
2
+ import type { IMaybeNotificationResponse } from '../../../core';
3
+ import * as i0 from "@angular/core";
4
+ export declare class LastNotificationResponseService {
5
+ private readonly injector;
6
+ private readonly lastResponse;
7
+ private isConnected;
8
+ connect(): Signal<IMaybeNotificationResponse>;
9
+ static ɵfac: i0.ɵɵFactoryDeclaration<LastNotificationResponseService, never>;
10
+ static ɵprov: i0.ɵɵInjectableDeclaration<LastNotificationResponseService>;
11
+ }
@@ -0,0 +1,37 @@
1
+ // Angular twin of `../../../react`'s `useLastNotificationResponse`, `connect()` shape combining
2
+ // `PermissionsService`'s signal return with `ScreenshotListenerService`'s effect+injector cleanup
3
+ import { effect, Injectable, Injector, signal, inject, } from '@angular/core';
4
+ import { addNotificationResponseClearedListener, addNotificationResponseReceivedListener, determineNextResponse, getLastNotificationResponse, } from '../../../core';
5
+ import * as i0 from "@angular/core";
6
+ export class LastNotificationResponseService {
7
+ injector = inject(Injector);
8
+ lastResponse = signal(undefined, /* @ts-ignore */
9
+ ...(ngDevMode ? [{ debugName: "lastResponse" }] : /* istanbul ignore next */ []));
10
+ isConnected = false;
11
+ connect() {
12
+ if (!this.isConnected) {
13
+ this.isConnected = true;
14
+ effect(onCleanup => {
15
+ this.lastResponse.update(prev => determineNextResponse(prev, getLastNotificationResponse()));
16
+ const subscription = addNotificationResponseReceivedListener(next => {
17
+ this.lastResponse.update(prev => determineNextResponse(prev, next));
18
+ });
19
+ const clearedSubscription = addNotificationResponseClearedListener(() => {
20
+ this.lastResponse.set(null);
21
+ });
22
+ onCleanup(() => {
23
+ subscription.remove();
24
+ clearedSubscription.remove();
25
+ });
26
+ }, { injector: this.injector });
27
+ }
28
+ return this.lastResponse.asReadonly();
29
+ }
30
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: LastNotificationResponseService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
31
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: LastNotificationResponseService, providedIn: 'root' });
32
+ }
33
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: LastNotificationResponseService, decorators: [{
34
+ type: Injectable,
35
+ args: [{ providedIn: 'root' }]
36
+ }] });
37
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/angular/services/last-notification-response.service/index.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,kGAAkG;AAElG,OAAO,EACL,MAAM,EACN,UAAU,EACV,QAAQ,EACR,MAAM,EACN,MAAM,GAEP,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,sCAAsC,EACtC,uCAAuC,EACvC,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,eAAe,CAAC;;AAIvB,MAAM,OAAO,+BAA+B;IACzB,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC5B,YAAY,GAAG,MAAM,CAA6B,SAAS;qFAAC,CAAC;IACtE,WAAW,GAAG,KAAK,CAAC;IAE5B,OAAO;QACL,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;YACxB,MAAM,CACJ,SAAS,CAAC,EAAE;gBACV,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAC9B,qBAAqB,CAAC,IAAI,EAAE,2BAA2B,EAAE,CAAC,CAC3D,CAAC;gBAEF,MAAM,YAAY,GAAG,uCAAuC,CAAC,IAAI,CAAC,EAAE;oBAClE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;gBACtE,CAAC,CAAC,CAAC;gBACH,MAAM,mBAAmB,GAAG,sCAAsC,CAChE,GAAG,EAAE;oBACH,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBAC9B,CAAC,CACF,CAAC;gBAEF,SAAS,CAAC,GAAG,EAAE;oBACb,YAAY,CAAC,MAAM,EAAE,CAAC;oBACtB,mBAAmB,CAAC,MAAM,EAAE,CAAC;gBAC/B,CAAC,CAAC,CAAC;YACL,CAAC,EACD,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAC5B,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,CAAC;IACxC,CAAC;uGAhCU,+BAA+B;2GAA/B,+BAA+B,cADlB,MAAM;;2FACnB,+BAA+B;kBAD3C,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE"}
@@ -0,0 +1,5 @@
1
+ export type IDevicePushTokenRegistration = {
2
+ isEnabled: boolean;
3
+ };
4
+ export declare function handlePersistedRegistrationInfoAsync(registrationInfo: string | null | undefined): Promise<void>;
5
+ export declare function installPushTokenAutoRegistration(): () => void;