@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.
- package/README.md +94 -58
- package/build/angular/index.d.ts +1 -0
- package/build/angular/index.js +1 -0
- package/build/angular/services/last-notification-response.service/index.d.ts +8 -0
- package/build/angular/services/last-notification-response.service/index.js +78 -0
- package/build/core/auto-registration.d.ts +5 -0
- package/build/core/auto-registration.js +65 -0
- package/build/core/backoff.d.ts +6 -0
- package/build/core/backoff.js +15 -0
- package/build/core/device-token.d.ts +7 -0
- package/build/core/device-token.js +35 -0
- package/build/core/emitter.d.ts +4 -0
- package/build/core/emitter.js +8 -0
- package/build/core/index.d.ts +3 -1
- package/build/core/index.js +3 -1
- package/build/core/last-notification-response.d.ts +2 -0
- package/build/core/last-notification-response.js +12 -0
- package/build/core/registration-abort.d.ts +2 -0
- package/build/core/registration-abort.js +10 -0
- package/build/core/token-metadata.d.ts +4 -0
- package/build/core/token-metadata.js +34 -0
- package/build/core/tokens.d.ts +4 -22
- package/build/core/tokens.js +41 -99
- package/build/core/update-device-push-token.d.ts +3 -0
- package/build/core/update-device-push-token.js +122 -0
- package/build/react/hooks/use-last-notification-response/index.d.ts +2 -0
- package/build/react/hooks/use-last-notification-response/index.js +22 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.js +2 -0
- package/build/solid/index.d.ts +2 -0
- package/build/solid/index.js +2 -0
- package/build/solid/primitives/create-last-notification-response.d.ts +3 -0
- package/build/solid/primitives/create-last-notification-response.js +20 -0
- package/build/svelte/index.d.ts +2 -0
- package/build/svelte/index.js +2 -0
- package/build/svelte/use-last-notification-response.svelte.d.ts +4 -0
- package/build/svelte/use-last-notification-response.svelte.js +25 -0
- package/build/vue/composables/use-last-notification-response/index.d.ts +3 -0
- package/build/vue/composables/use-last-notification-response/index.js +20 -0
- package/build/vue/index.d.ts +2 -0
- package/build/vue/index.js +2 -0
- package/build-ngc/angular/index.d.ts +1 -0
- package/build-ngc/angular/index.js +1 -0
- package/build-ngc/angular/index.js.map +1 -1
- package/build-ngc/angular/services/last-notification-response.service/index.d.ts +11 -0
- package/build-ngc/angular/services/last-notification-response.service/index.js +37 -0
- package/build-ngc/angular/services/last-notification-response.service/index.js.map +1 -0
- package/build-ngc/core/auto-registration.d.ts +5 -0
- package/build-ngc/core/auto-registration.js +66 -0
- package/build-ngc/core/auto-registration.js.map +1 -0
- package/build-ngc/core/backoff.d.ts +6 -0
- package/build-ngc/core/backoff.js +16 -0
- package/build-ngc/core/backoff.js.map +1 -0
- package/build-ngc/core/device-token.d.ts +7 -0
- package/build-ngc/core/device-token.js +36 -0
- package/build-ngc/core/device-token.js.map +1 -0
- package/build-ngc/core/emitter.d.ts +4 -0
- package/build-ngc/core/emitter.js +8 -0
- package/build-ngc/core/emitter.js.map +1 -1
- package/build-ngc/core/index.d.ts +3 -1
- package/build-ngc/core/index.js +3 -1
- package/build-ngc/core/index.js.map +1 -1
- package/build-ngc/core/last-notification-response.d.ts +2 -0
- package/build-ngc/core/last-notification-response.js +13 -0
- package/build-ngc/core/last-notification-response.js.map +1 -0
- package/build-ngc/core/registration-abort.d.ts +2 -0
- package/build-ngc/core/registration-abort.js +11 -0
- package/build-ngc/core/registration-abort.js.map +1 -0
- package/build-ngc/core/token-metadata.d.ts +4 -0
- package/build-ngc/core/token-metadata.js +35 -0
- package/build-ngc/core/token-metadata.js.map +1 -0
- package/build-ngc/core/tokens.d.ts +4 -22
- package/build-ngc/core/tokens.js +41 -99
- package/build-ngc/core/tokens.js.map +1 -1
- package/build-ngc/core/update-device-push-token.d.ts +3 -0
- package/build-ngc/core/update-device-push-token.js +123 -0
- package/build-ngc/core/update-device-push-token.js.map +1 -0
- package/package.json +24 -23
- package/src/angular/index.ts +1 -0
- package/src/angular/services/last-notification-response.service/index.ts +54 -0
- package/src/core/auto-registration.ts +105 -0
- package/src/core/backoff.ts +24 -0
- package/src/core/device-token.ts +54 -0
- package/src/core/emitter.ts +10 -0
- package/src/core/index.ts +8 -0
- package/src/core/last-notification-response.ts +19 -0
- package/src/core/registration-abort.ts +12 -0
- package/src/core/token-metadata.ts +42 -0
- package/src/core/tokens.ts +71 -133
- package/src/core/update-device-push-token.ts +173 -0
- package/src/react/hooks/use-last-notification-response/index.ts +35 -0
- package/src/react/index.ts +2 -0
- package/src/solid/index.ts +2 -0
- package/src/solid/primitives/create-last-notification-response.ts +35 -0
- package/src/svelte/index.ts +2 -0
- package/src/svelte/svelte-ambient.d.ts +3 -0
- package/src/svelte/svelte-compile.test-helper.ts +7 -0
- package/src/svelte/use-last-notification-response.svelte.ts +45 -0
- package/src/vue/composables/use-last-notification-response/index.ts +35 -0
- package/src/vue/index.ts +2 -0
package/src/core/tokens.ts
CHANGED
|
@@ -1,78 +1,35 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
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 {
|
|
12
|
-
CodedError,
|
|
13
|
-
Platform,
|
|
14
|
-
UnavailabilityError,
|
|
15
|
-
type EventSubscription,
|
|
16
|
-
} 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';
|
|
17
4
|
|
|
18
5
|
import { dlog } from '@symbiote-native/engine';
|
|
19
6
|
|
|
7
|
+
import { installPushTokenAutoRegistration } from './auto-registration';
|
|
8
|
+
import { getDevicePushTokenAsync } from './device-token';
|
|
20
9
|
import {
|
|
21
10
|
pushTokenManager,
|
|
22
11
|
serverRegistrationModule,
|
|
23
12
|
topicSubscriptionModule,
|
|
24
13
|
} from './native-modules';
|
|
14
|
+
import { abortPendingRegistration } from './registration-abort';
|
|
15
|
+
import {
|
|
16
|
+
getDeviceIdAsync,
|
|
17
|
+
getTypeOfToken,
|
|
18
|
+
shouldUseDevelopmentNotificationService,
|
|
19
|
+
} from './token-metadata';
|
|
25
20
|
import type {
|
|
26
21
|
IDevicePushToken,
|
|
27
22
|
IExpoPushToken,
|
|
28
23
|
IExpoPushTokenOptions,
|
|
29
24
|
} from './types';
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
37
|
-
return Platform.OS;
|
|
38
|
-
}
|
|
39
|
-
throw new Error(`[notifications] unsupported platform: ${Platform.OS}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type IPushTokenListener = (token: IDevicePushToken) => void;
|
|
43
|
-
|
|
44
|
-
/** Fires whenever the OS rolls the device push token while the app is running. */
|
|
45
|
-
export function addPushTokenListener(
|
|
46
|
-
listener: IPushTokenListener,
|
|
47
|
-
): EventSubscription {
|
|
48
|
-
return pushTokenManager.addListener(
|
|
49
|
-
'onDevicePushToken',
|
|
50
|
-
({ devicePushToken }) => {
|
|
51
|
-
listener({ data: devicePushToken, type: nativePlatform() });
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
let nativeTokenPromise: Promise<string> | null = null;
|
|
26
|
+
export {
|
|
27
|
+
addPushTokenListener,
|
|
28
|
+
getDevicePushTokenAsync,
|
|
29
|
+
type IPushTokenListener,
|
|
30
|
+
} from './device-token';
|
|
57
31
|
|
|
58
|
-
|
|
59
|
-
export async function getDevicePushTokenAsync(): Promise<IDevicePushToken> {
|
|
60
|
-
if (!pushTokenManager.getDevicePushTokenAsync) {
|
|
61
|
-
throw new UnavailabilityError('Notifications', 'getDevicePushTokenAsync');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let devicePushToken: string;
|
|
65
|
-
if (nativeTokenPromise) {
|
|
66
|
-
devicePushToken = await nativeTokenPromise;
|
|
67
|
-
} else {
|
|
68
|
-
nativeTokenPromise = pushTokenManager.getDevicePushTokenAsync();
|
|
69
|
-
devicePushToken = await nativeTokenPromise;
|
|
70
|
-
nativeTokenPromise = null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
dlog('[notifications] getDevicePushTokenAsync resolved');
|
|
74
|
-
return { type: nativePlatform(), data: devicePushToken };
|
|
75
|
-
}
|
|
32
|
+
const PRODUCTION_BASE_URL = 'https://exp.host/--/api/v2/';
|
|
76
33
|
|
|
77
34
|
/** Unregisters the device from receiving remote push notifications entirely. */
|
|
78
35
|
export async function unregisterForNotificationsAsync(): Promise<void> {
|
|
@@ -101,14 +58,13 @@ export async function unsubscribeFromTopicAsync(topic: string): Promise<null> {
|
|
|
101
58
|
return topicSubscriptionModule.unsubscribeFromTopicAsync(topic);
|
|
102
59
|
}
|
|
103
60
|
|
|
104
|
-
/**
|
|
105
|
-
* Enables/disables letting the OS-level registration blob persist whether the device push
|
|
106
|
-
* token should keep being pushed to the Expo push service backend. See the module doc comment
|
|
107
|
-
* above for what this deliberately does *not* do (the auto-resync daemon).
|
|
108
|
-
*/
|
|
61
|
+
/** Persists whether the device push token keeps being pushed to the Expo push service */
|
|
109
62
|
export async function setAutoServerRegistrationEnabledAsync(
|
|
110
63
|
enabled: boolean,
|
|
111
64
|
): Promise<void> {
|
|
65
|
+
// Перезаписываем регистрацию, поэтому висящий запрос не должен завершиться
|
|
66
|
+
abortPendingRegistration();
|
|
67
|
+
|
|
112
68
|
if (!serverRegistrationModule.setRegistrationInfoAsync) {
|
|
113
69
|
throw new UnavailabilityError('Notifications', 'setRegistrationInfoAsync');
|
|
114
70
|
}
|
|
@@ -131,29 +87,7 @@ export async function setAutoServerRegistrationEnabledAsync(
|
|
|
131
87
|
await serverRegistrationModule.setRegistrationInfoAsync(
|
|
132
88
|
JSON.stringify(existing),
|
|
133
89
|
);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
async function getDeviceIdAsync(): Promise<string> {
|
|
137
|
-
try {
|
|
138
|
-
if (!serverRegistrationModule.getInstallationIdAsync) {
|
|
139
|
-
throw new UnavailabilityError('Notifications', 'getInstallationIdAsync');
|
|
140
|
-
}
|
|
141
|
-
return await serverRegistrationModule.getInstallationIdAsync();
|
|
142
|
-
} catch (error) {
|
|
143
|
-
throw new CodedError(
|
|
144
|
-
'ERR_NOTIF_DEVICE_ID',
|
|
145
|
-
`Could not fetch the installation ID of the application: ${error}.`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function getTypeOfToken(devicePushToken: IDevicePushToken): string {
|
|
151
|
-
switch (devicePushToken.type) {
|
|
152
|
-
case 'ios':
|
|
153
|
-
return 'apns';
|
|
154
|
-
case 'android':
|
|
155
|
-
return 'fcm';
|
|
156
|
-
}
|
|
90
|
+
installPushTokenAutoRegistration();
|
|
157
91
|
}
|
|
158
92
|
|
|
159
93
|
function getDeviceToken(devicePushToken: IDevicePushToken): string {
|
|
@@ -189,50 +123,10 @@ function extractExpoPushToken(data: unknown): string {
|
|
|
189
123
|
);
|
|
190
124
|
}
|
|
191
125
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
*
|
|
197
|
-
* ponytail: upstream auto-fills `projectId`/`applicationId`/`development` from
|
|
198
|
-
* `expo-constants`/`expo-application`. Neither package is part of this port, so pass them
|
|
199
|
-
* explicitly (`applicationId` from `@symbiote-native/application`'s own `applicationId` export
|
|
200
|
-
* covers the second one).
|
|
201
|
-
*/
|
|
202
|
-
export async function getExpoPushTokenAsync(
|
|
203
|
-
options: IExpoPushTokenOptions = {},
|
|
204
|
-
): Promise<IExpoPushToken> {
|
|
205
|
-
const devicePushToken =
|
|
206
|
-
options.devicePushToken ?? (await getDevicePushTokenAsync());
|
|
207
|
-
const deviceId = options.deviceId ?? (await getDeviceIdAsync());
|
|
208
|
-
|
|
209
|
-
if (!options.projectId) {
|
|
210
|
-
throw new CodedError(
|
|
211
|
-
'ERR_NOTIFICATIONS_NO_EXPERIENCE_ID',
|
|
212
|
-
'No "projectId" was passed to getExpoPushTokenAsync(). This port does not read expo-constants — pass it explicitly.',
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
if (!options.applicationId) {
|
|
216
|
-
throw new CodedError(
|
|
217
|
-
'ERR_NOTIFICATIONS_NO_APPLICATION_ID',
|
|
218
|
-
'No "applicationId" was passed to getExpoPushTokenAsync(). This port does not read expo-application — pass it explicitly (e.g. @symbiote-native/application\'s `applicationId`).',
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const type = options.type ?? getTypeOfToken(devicePushToken);
|
|
223
|
-
const development = options.development ?? false;
|
|
224
|
-
const baseUrl = options.baseUrl ?? PRODUCTION_BASE_URL;
|
|
225
|
-
const url = options.url ?? `${baseUrl}push/getExpoPushToken`;
|
|
226
|
-
|
|
227
|
-
const body = {
|
|
228
|
-
type,
|
|
229
|
-
deviceId: deviceId.toLowerCase(),
|
|
230
|
-
development,
|
|
231
|
-
appId: options.applicationId,
|
|
232
|
-
deviceToken: getDeviceToken(devicePushToken),
|
|
233
|
-
projectId: options.projectId,
|
|
234
|
-
};
|
|
235
|
-
|
|
126
|
+
async function requestExpoPushTokenAsync(
|
|
127
|
+
url: string,
|
|
128
|
+
body: Record<string, unknown>,
|
|
129
|
+
): Promise<string> {
|
|
236
130
|
let response: Response;
|
|
237
131
|
try {
|
|
238
132
|
response = await fetch(url, {
|
|
@@ -254,7 +148,51 @@ export async function getExpoPushTokenAsync(
|
|
|
254
148
|
);
|
|
255
149
|
}
|
|
256
150
|
|
|
257
|
-
|
|
151
|
+
return extractExpoPushToken(await parseJsonResponse(response));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Вынесено, чтобы `getExpoPushTokenAsync` читался как запрос токена и регистрация
|
|
155
|
+
async function buildExpoPushTokenBody(
|
|
156
|
+
options: IExpoPushTokenOptions,
|
|
157
|
+
devicePushToken: IDevicePushToken,
|
|
158
|
+
): Promise<Record<string, unknown>> {
|
|
159
|
+
if (!options.projectId) {
|
|
160
|
+
throw new CodedError(
|
|
161
|
+
'ERR_NOTIFICATIONS_NO_EXPERIENCE_ID',
|
|
162
|
+
'No "projectId" was passed to getExpoPushTokenAsync(). Pass it explicitly (this port has no expo-constants).',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const appId = options.applicationId ?? applicationId;
|
|
166
|
+
if (!appId) {
|
|
167
|
+
throw new CodedError(
|
|
168
|
+
'ERR_NOTIFICATIONS_NO_APPLICATION_ID',
|
|
169
|
+
'No "applicationId" was passed to getExpoPushTokenAsync() and the app has none of its own.',
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const deviceId = options.deviceId ?? (await getDeviceIdAsync());
|
|
174
|
+
return {
|
|
175
|
+
type: options.type ?? getTypeOfToken(devicePushToken),
|
|
176
|
+
deviceId: deviceId.toLowerCase(),
|
|
177
|
+
development:
|
|
178
|
+
options.development ?? (await shouldUseDevelopmentNotificationService()),
|
|
179
|
+
appId,
|
|
180
|
+
deviceToken: getDeviceToken(devicePushToken),
|
|
181
|
+
projectId: options.projectId,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Returns an Expo push token for Expo's push relay, needs network so retry when back online */
|
|
186
|
+
export async function getExpoPushTokenAsync(
|
|
187
|
+
options: IExpoPushTokenOptions = {},
|
|
188
|
+
): Promise<IExpoPushToken> {
|
|
189
|
+
const devicePushToken =
|
|
190
|
+
options.devicePushToken ?? (await getDevicePushTokenAsync());
|
|
191
|
+
const body = await buildExpoPushTokenBody(options, devicePushToken);
|
|
192
|
+
const baseUrl = options.baseUrl ?? PRODUCTION_BASE_URL;
|
|
193
|
+
const url = options.url ?? `${baseUrl}push/getExpoPushToken`;
|
|
194
|
+
|
|
195
|
+
const expoPushToken = await requestExpoPushTokenAsync(url, body);
|
|
258
196
|
|
|
259
197
|
if (!options.url && !options.baseUrl) {
|
|
260
198
|
try {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Порт `utils/updateDevicePushTokenAsync.ts` из upstream sdk-57
|
|
2
|
+
import { applicationId } from '@symbiote-native/application';
|
|
3
|
+
import { dlog } from '@symbiote-native/engine';
|
|
4
|
+
|
|
5
|
+
import { computeNextBackoffInterval } from './backoff';
|
|
6
|
+
import { serverRegistrationModule } from './native-modules';
|
|
7
|
+
import {
|
|
8
|
+
getDeviceIdAsync,
|
|
9
|
+
getTypeOfToken,
|
|
10
|
+
shouldUseDevelopmentNotificationService,
|
|
11
|
+
} from './token-metadata';
|
|
12
|
+
import type { IDevicePushToken } from './types';
|
|
13
|
+
|
|
14
|
+
const UPDATE_DEVICE_PUSH_TOKEN_URL =
|
|
15
|
+
'https://exp.host/--/api/v2/push/updateDeviceToken';
|
|
16
|
+
const LAST_TOKEN_KEY = 'lastRegisteredDeviceToken';
|
|
17
|
+
const INITIAL_BACKOFF_MS = 500;
|
|
18
|
+
const MAX_BACKOFF_MS = 2 * 60 * 1000;
|
|
19
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
// Повторная регистрация раз в 7 дней, т.к. сервер мог потерять запись об устройстве
|
|
22
|
+
const REGISTRATION_TTL_MS = 7 * MS_PER_DAY;
|
|
23
|
+
|
|
24
|
+
type IStoredTokenData = {
|
|
25
|
+
deviceToken: string;
|
|
26
|
+
appId: string | null;
|
|
27
|
+
development: boolean;
|
|
28
|
+
type: string;
|
|
29
|
+
registeredAt: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
33
|
+
return typeof value === 'object' && value !== null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isStoredTokenData(value: unknown): value is IStoredTokenData {
|
|
37
|
+
return (
|
|
38
|
+
isRecord(value) &&
|
|
39
|
+
typeof value.deviceToken === 'string' &&
|
|
40
|
+
typeof value.development === 'boolean' &&
|
|
41
|
+
typeof value.type === 'string' &&
|
|
42
|
+
typeof value.registeredAt === 'number'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getLastRegisteredTokenDataAsync(): Promise<IStoredTokenData | null> {
|
|
47
|
+
try {
|
|
48
|
+
if (!serverRegistrationModule.getRegistrationInfoAsync) return null;
|
|
49
|
+
const info = await serverRegistrationModule.getRegistrationInfoAsync();
|
|
50
|
+
if (!info) return null;
|
|
51
|
+
const stored: unknown = JSON.parse(info)?.[LAST_TOKEN_KEY];
|
|
52
|
+
return isStoredTokenData(stored) ? stored : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function setLastRegisteredTokenDataAsync(
|
|
59
|
+
tokenData: IStoredTokenData,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
try {
|
|
62
|
+
if (
|
|
63
|
+
!serverRegistrationModule.getRegistrationInfoAsync ||
|
|
64
|
+
!serverRegistrationModule.setRegistrationInfoAsync
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const info = await serverRegistrationModule.getRegistrationInfoAsync();
|
|
69
|
+
const existing = info ? JSON.parse(info) : {};
|
|
70
|
+
existing[LAST_TOKEN_KEY] = tokenData;
|
|
71
|
+
await serverRegistrationModule.setRegistrationInfoAsync(
|
|
72
|
+
JSON.stringify(existing),
|
|
73
|
+
);
|
|
74
|
+
} catch {
|
|
75
|
+
// Best-effort, при следующем запуске приложение зарегистрируется снова
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Если проверка невозможна, считаем токен изменившимся (fail-open)
|
|
80
|
+
export async function hasDeviceTokenChangedAsync(
|
|
81
|
+
token: IDevicePushToken,
|
|
82
|
+
): Promise<boolean> {
|
|
83
|
+
try {
|
|
84
|
+
const development = await shouldUseDevelopmentNotificationService();
|
|
85
|
+
const lastTokenData = await getLastRegisteredTokenDataAsync();
|
|
86
|
+
|
|
87
|
+
if (lastTokenData == null) return true;
|
|
88
|
+
|
|
89
|
+
const age = Date.now() - lastTokenData.registeredAt;
|
|
90
|
+
if (age < 0 || age >= REGISTRATION_TTL_MS) return true;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
token.data !== lastTokenData.deviceToken ||
|
|
94
|
+
applicationId !== lastTokenData.appId ||
|
|
95
|
+
development !== lastTokenData.development ||
|
|
96
|
+
getTypeOfToken(token) !== lastTokenData.type
|
|
97
|
+
);
|
|
98
|
+
} catch {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// `true` значит запрос надо повторить
|
|
104
|
+
async function sendTokenOnceAsync(
|
|
105
|
+
signal: AbortSignal,
|
|
106
|
+
token: IDevicePushToken,
|
|
107
|
+
): Promise<boolean> {
|
|
108
|
+
const [development, deviceId] = await Promise.all([
|
|
109
|
+
shouldUseDevelopmentNotificationService(),
|
|
110
|
+
getDeviceIdAsync(),
|
|
111
|
+
]);
|
|
112
|
+
const type = getTypeOfToken(token);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(UPDATE_DEVICE_PUSH_TOKEN_URL, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { 'content-type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
deviceId: deviceId.toLowerCase(),
|
|
120
|
+
development,
|
|
121
|
+
deviceToken: token.data,
|
|
122
|
+
appId: applicationId,
|
|
123
|
+
type,
|
|
124
|
+
}),
|
|
125
|
+
signal,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
dlog(
|
|
130
|
+
`[notifications] device push token update rejected: ${await response.text()}`,
|
|
131
|
+
);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await setLastRegisteredTokenDataAsync({
|
|
136
|
+
deviceToken: token.data,
|
|
137
|
+
appId: applicationId,
|
|
138
|
+
development,
|
|
139
|
+
type,
|
|
140
|
+
registeredAt: Date.now(),
|
|
141
|
+
});
|
|
142
|
+
return false;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// Прерванный запрос может упасть с `AbortError` или нативной ошибкой отмены
|
|
145
|
+
if (signal.aborted || (isRecord(error) && error.name === 'AbortError'))
|
|
146
|
+
return false;
|
|
147
|
+
|
|
148
|
+
console.warn(
|
|
149
|
+
'[notifications] Error thrown while updating the device push token with the server:',
|
|
150
|
+
error,
|
|
151
|
+
);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function updateDevicePushTokenAsync(
|
|
157
|
+
signal: AbortSignal,
|
|
158
|
+
token: IDevicePushToken,
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const backoffOptions = { maxBackoff: MAX_BACKOFF_MS };
|
|
161
|
+
|
|
162
|
+
for (let retriesCount = 0; !signal.aborted; retriesCount += 1) {
|
|
163
|
+
const shouldRetry = await sendTokenOnceAsync(signal, token);
|
|
164
|
+
if (!shouldRetry || signal.aborted) return;
|
|
165
|
+
|
|
166
|
+
const backoff = computeNextBackoffInterval(
|
|
167
|
+
INITIAL_BACKOFF_MS,
|
|
168
|
+
retriesCount,
|
|
169
|
+
backoffOptions,
|
|
170
|
+
);
|
|
171
|
+
await new Promise<void>(resolve => setTimeout(resolve, backoff));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// React twin of expo-notifications' `useLastNotificationResponse`. `useLayoutEffect` matches
|
|
2
|
+
// upstream's own choice - the listener must register before paint
|
|
3
|
+
|
|
4
|
+
import { useLayoutEffect, useState } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
addNotificationResponseClearedListener,
|
|
7
|
+
addNotificationResponseReceivedListener,
|
|
8
|
+
determineNextResponse,
|
|
9
|
+
getLastNotificationResponse,
|
|
10
|
+
} from '../../../core';
|
|
11
|
+
import type { IMaybeNotificationResponse } from '../../../core';
|
|
12
|
+
|
|
13
|
+
export function useLastNotificationResponse(): IMaybeNotificationResponse {
|
|
14
|
+
const [lastResponse, setLastResponse] =
|
|
15
|
+
useState<IMaybeNotificationResponse>(undefined);
|
|
16
|
+
|
|
17
|
+
useLayoutEffect(() => {
|
|
18
|
+
const response = getLastNotificationResponse();
|
|
19
|
+
setLastResponse(prev => determineNextResponse(prev, response));
|
|
20
|
+
|
|
21
|
+
const subscription = addNotificationResponseReceivedListener(next => {
|
|
22
|
+
setLastResponse(prev => determineNextResponse(prev, next));
|
|
23
|
+
});
|
|
24
|
+
const clearedSubscription = addNotificationResponseClearedListener(() => {
|
|
25
|
+
setLastResponse(null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
subscription.remove();
|
|
30
|
+
clearedSubscription.remove();
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
return lastResponse;
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Solid twin of `../../react`'s `useLastNotificationResponse`
|
|
2
|
+
|
|
3
|
+
import { createSignal, onCleanup, onMount, type Accessor } from 'solid-js';
|
|
4
|
+
import {
|
|
5
|
+
addNotificationResponseClearedListener,
|
|
6
|
+
addNotificationResponseReceivedListener,
|
|
7
|
+
determineNextResponse,
|
|
8
|
+
getLastNotificationResponse,
|
|
9
|
+
} from '../../core';
|
|
10
|
+
import type { IMaybeNotificationResponse } from '../../core';
|
|
11
|
+
|
|
12
|
+
export function createLastNotificationResponse(): Accessor<IMaybeNotificationResponse> {
|
|
13
|
+
const [lastResponse, setLastResponse] =
|
|
14
|
+
createSignal<IMaybeNotificationResponse>(undefined);
|
|
15
|
+
|
|
16
|
+
onMount(() => {
|
|
17
|
+
setLastResponse(prev =>
|
|
18
|
+
determineNextResponse(prev, getLastNotificationResponse()),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const subscription = addNotificationResponseReceivedListener(next => {
|
|
22
|
+
setLastResponse(prev => determineNextResponse(prev, next));
|
|
23
|
+
});
|
|
24
|
+
const clearedSubscription = addNotificationResponseClearedListener(() => {
|
|
25
|
+
setLastResponse(null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
onCleanup(() => {
|
|
29
|
+
subscription.remove();
|
|
30
|
+
clearedSubscription.remove();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return lastResponse;
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Re-exports the existing harness instead of a third copy (packages/sqlite and
|
|
2
|
+
// packages/navigation already carry one, not part of either package's public exports)
|
|
3
|
+
export {
|
|
4
|
+
createSvelteHarness,
|
|
5
|
+
loadComponent,
|
|
6
|
+
type ISvelteHarness,
|
|
7
|
+
} from '../../../sqlite/src/svelte/svelte-compile.test-helper';
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
|
|
4
|
+
import { untrack } from 'svelte';
|
|
5
|
+
import {
|
|
6
|
+
addNotificationResponseClearedListener,
|
|
7
|
+
addNotificationResponseReceivedListener,
|
|
8
|
+
determineNextResponse,
|
|
9
|
+
getLastNotificationResponse,
|
|
10
|
+
} from '../core';
|
|
11
|
+
import type { IMaybeNotificationResponse } from '../core';
|
|
12
|
+
|
|
13
|
+
export function useLastNotificationResponse(): {
|
|
14
|
+
readonly current: IMaybeNotificationResponse;
|
|
15
|
+
} {
|
|
16
|
+
let lastResponse = $state<IMaybeNotificationResponse>(undefined);
|
|
17
|
+
|
|
18
|
+
$effect(() => {
|
|
19
|
+
lastResponse = determineNextResponse(
|
|
20
|
+
untrack(() => lastResponse),
|
|
21
|
+
getLastNotificationResponse(),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const subscription = addNotificationResponseReceivedListener(next => {
|
|
25
|
+
lastResponse = determineNextResponse(
|
|
26
|
+
untrack(() => lastResponse),
|
|
27
|
+
next,
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
const clearedSubscription = addNotificationResponseClearedListener(() => {
|
|
31
|
+
lastResponse = null;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
subscription.remove();
|
|
36
|
+
clearedSubscription.remove();
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
get current(): IMaybeNotificationResponse {
|
|
42
|
+
return lastResponse;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Vue twin of `../../../react`'s `useLastNotificationResponse`
|
|
2
|
+
|
|
3
|
+
import { onMounted, onUnmounted, ref, type Ref } from '@vue/runtime-core';
|
|
4
|
+
import {
|
|
5
|
+
addNotificationResponseClearedListener,
|
|
6
|
+
addNotificationResponseReceivedListener,
|
|
7
|
+
determineNextResponse,
|
|
8
|
+
getLastNotificationResponse,
|
|
9
|
+
} from '../../../core';
|
|
10
|
+
import type { IMaybeNotificationResponse } from '../../../core';
|
|
11
|
+
|
|
12
|
+
export function useLastNotificationResponse(): Ref<IMaybeNotificationResponse> {
|
|
13
|
+
const lastResponse = ref<IMaybeNotificationResponse>(undefined);
|
|
14
|
+
|
|
15
|
+
onMounted(() => {
|
|
16
|
+
lastResponse.value = determineNextResponse(
|
|
17
|
+
lastResponse.value,
|
|
18
|
+
getLastNotificationResponse(),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const subscription = addNotificationResponseReceivedListener(next => {
|
|
22
|
+
lastResponse.value = determineNextResponse(lastResponse.value, next);
|
|
23
|
+
});
|
|
24
|
+
const clearedSubscription = addNotificationResponseClearedListener(() => {
|
|
25
|
+
lastResponse.value = null;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
onUnmounted(() => {
|
|
29
|
+
subscription.remove();
|
|
30
|
+
clearedSubscription.remove();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return lastResponse;
|
|
35
|
+
}
|
package/src/vue/index.ts
ADDED