@symbiote-native/notifications 0.1.2 → 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
|
@@ -0,0 +1,123 @@
|
|
|
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';
|
|
5
|
+
import { serverRegistrationModule } from './native-modules';
|
|
6
|
+
import { getDeviceIdAsync, getTypeOfToken, shouldUseDevelopmentNotificationService, } from './token-metadata';
|
|
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
|
+
}
|
|
123
|
+
//# sourceMappingURL=update-device-push-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-device-push-token.js","sourceRoot":"","sources":["../../src/core/update-device-push-token.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/notifications",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "expo-notifications wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Permissions, device/Expo push tokens, scheduling, presentation, badges, Android channels/channel groups, categories, and the background notification task hook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,20 +35,20 @@
|
|
|
35
35
|
"default": "./build/core/index.js"
|
|
36
36
|
},
|
|
37
37
|
"./vue": {
|
|
38
|
-
"types": "./build/
|
|
39
|
-
"default": "./build/
|
|
38
|
+
"types": "./build/vue/index.d.ts",
|
|
39
|
+
"default": "./build/vue/index.js"
|
|
40
40
|
},
|
|
41
41
|
"./react": {
|
|
42
|
-
"types": "./build/
|
|
43
|
-
"default": "./build/
|
|
42
|
+
"types": "./build/react/index.d.ts",
|
|
43
|
+
"default": "./build/react/index.js"
|
|
44
44
|
},
|
|
45
45
|
"./svelte": {
|
|
46
|
-
"types": "./build/
|
|
47
|
-
"default": "./build/
|
|
46
|
+
"types": "./build/svelte/index.d.ts",
|
|
47
|
+
"default": "./build/svelte/index.js"
|
|
48
48
|
},
|
|
49
49
|
"./solid": {
|
|
50
|
-
"types": "./build/
|
|
51
|
-
"default": "./build/
|
|
50
|
+
"types": "./build/solid/index.d.ts",
|
|
51
|
+
"default": "./build/solid/index.js"
|
|
52
52
|
},
|
|
53
53
|
"./angular": {
|
|
54
54
|
"types": "./build-ngc/angular/index.d.ts",
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"expo-notifications": "57.0.17",
|
|
73
|
-
"expo-modules-core": "57.0.
|
|
73
|
+
"expo-modules-core": "57.0.20",
|
|
74
|
+
"@symbiote-native/application": "3.0.3"
|
|
74
75
|
},
|
|
75
76
|
"peerDependencies": {
|
|
76
77
|
"@angular/core": ">=20",
|
|
@@ -80,12 +81,12 @@
|
|
|
80
81
|
"solid-js": ">=1.9.0",
|
|
81
82
|
"svelte": ">=5.56.0",
|
|
82
83
|
"vue": ">=3.5.0",
|
|
83
|
-
"@symbiote-native/angular": "^3.
|
|
84
|
-
"@symbiote-native/engine": "^1.
|
|
85
|
-
"@symbiote-native/react": "^3.0
|
|
86
|
-
"@symbiote-native/solid": "^3.0
|
|
87
|
-
"@symbiote-native/svelte": "^3.0
|
|
88
|
-
"@symbiote-native/vue": "^3.0
|
|
84
|
+
"@symbiote-native/angular": "^3.2.0",
|
|
85
|
+
"@symbiote-native/engine": "^1.5.0",
|
|
86
|
+
"@symbiote-native/react": "^3.2.0",
|
|
87
|
+
"@symbiote-native/solid": "^3.1.0",
|
|
88
|
+
"@symbiote-native/svelte": "^3.1.0",
|
|
89
|
+
"@symbiote-native/vue": "^3.2.0"
|
|
89
90
|
},
|
|
90
91
|
"peerDependenciesMeta": {
|
|
91
92
|
"@symbiote-native/angular": {
|
|
@@ -133,13 +134,13 @@
|
|
|
133
134
|
"solid-js": "^1.9.14",
|
|
134
135
|
"svelte": "^5.56.0",
|
|
135
136
|
"typescript": "~6.0.0",
|
|
136
|
-
"@symbiote-native/angular": "3.
|
|
137
|
-
"@symbiote-native/engine": "1.
|
|
138
|
-
"@symbiote-native/react": "3.0
|
|
139
|
-
"@symbiote-native/solid": "3.0
|
|
140
|
-
"@symbiote-native/svelte": "3.0
|
|
141
|
-
"@symbiote-native/test-utils": "0.4.
|
|
142
|
-
"@symbiote-native/vue": "3.0
|
|
137
|
+
"@symbiote-native/angular": "3.2.0",
|
|
138
|
+
"@symbiote-native/engine": "1.5.0",
|
|
139
|
+
"@symbiote-native/react": "3.2.0",
|
|
140
|
+
"@symbiote-native/solid": "3.1.0",
|
|
141
|
+
"@symbiote-native/svelte": "3.1.0",
|
|
142
|
+
"@symbiote-native/test-utils": "0.4.6",
|
|
143
|
+
"@symbiote-native/vue": "3.2.0"
|
|
143
144
|
},
|
|
144
145
|
"scripts": {
|
|
145
146
|
"typecheck": "tsc --build",
|
package/src/angular/index.ts
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Angular twin of `../../../react`'s `useLastNotificationResponse`, `connect()` shape combining
|
|
2
|
+
// `PermissionsService`'s signal return with `ScreenshotListenerService`'s effect+injector cleanup
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
effect,
|
|
6
|
+
Injectable,
|
|
7
|
+
Injector,
|
|
8
|
+
signal,
|
|
9
|
+
inject,
|
|
10
|
+
type Signal,
|
|
11
|
+
} from '@angular/core';
|
|
12
|
+
import {
|
|
13
|
+
addNotificationResponseClearedListener,
|
|
14
|
+
addNotificationResponseReceivedListener,
|
|
15
|
+
determineNextResponse,
|
|
16
|
+
getLastNotificationResponse,
|
|
17
|
+
} from '../../../core';
|
|
18
|
+
import type { IMaybeNotificationResponse } from '../../../core';
|
|
19
|
+
|
|
20
|
+
@Injectable({ providedIn: 'root' })
|
|
21
|
+
export class LastNotificationResponseService {
|
|
22
|
+
private readonly injector = inject(Injector);
|
|
23
|
+
private readonly lastResponse = signal<IMaybeNotificationResponse>(undefined);
|
|
24
|
+
private isConnected = false;
|
|
25
|
+
|
|
26
|
+
connect(): Signal<IMaybeNotificationResponse> {
|
|
27
|
+
if (!this.isConnected) {
|
|
28
|
+
this.isConnected = true;
|
|
29
|
+
effect(
|
|
30
|
+
onCleanup => {
|
|
31
|
+
this.lastResponse.update(prev =>
|
|
32
|
+
determineNextResponse(prev, getLastNotificationResponse()),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const subscription = addNotificationResponseReceivedListener(next => {
|
|
36
|
+
this.lastResponse.update(prev => determineNextResponse(prev, next));
|
|
37
|
+
});
|
|
38
|
+
const clearedSubscription = addNotificationResponseClearedListener(
|
|
39
|
+
() => {
|
|
40
|
+
this.lastResponse.set(null);
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
onCleanup(() => {
|
|
45
|
+
subscription.remove();
|
|
46
|
+
clearedSubscription.remove();
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
{ injector: this.injector },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return this.lastResponse.asReadonly();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Порт `DevicePushTokenAutoRegistration.fx.ts`, ресинк токена на сервер Expo
|
|
2
|
+
import { UnavailabilityError } from 'expo-modules-core';
|
|
3
|
+
|
|
4
|
+
import { addPushTokenListener, getDevicePushTokenAsync } from './device-token';
|
|
5
|
+
import { serverRegistrationModule } from './native-modules';
|
|
6
|
+
import { startAbortableRegistration } from './registration-abort';
|
|
7
|
+
import {
|
|
8
|
+
hasDeviceTokenChangedAsync,
|
|
9
|
+
updateDevicePushTokenAsync,
|
|
10
|
+
} from './update-device-push-token';
|
|
11
|
+
import type { IDevicePushToken } from './types';
|
|
12
|
+
|
|
13
|
+
export type IDevicePushTokenRegistration = {
|
|
14
|
+
isEnabled: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function updatePushTokenAsync(token: IDevicePushToken): Promise<void> {
|
|
18
|
+
if (!(await hasDeviceTokenChangedAsync(token))) return;
|
|
19
|
+
await updateDevicePushTokenAsync(startAbortableRegistration(), token);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isEnabledRegistration(info: string | null | undefined): boolean {
|
|
23
|
+
if (!info) return false;
|
|
24
|
+
try {
|
|
25
|
+
const registration: IDevicePushTokenRegistration | null = JSON.parse(info);
|
|
26
|
+
return registration?.isEnabled === true;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.warn(
|
|
29
|
+
'[notifications] Error encountered while fetching registration information for auto token updates.',
|
|
30
|
+
error,
|
|
31
|
+
);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Досылает токен, если регистрация включена, а сервер его не получил
|
|
37
|
+
export async function handlePersistedRegistrationInfoAsync(
|
|
38
|
+
registrationInfo: string | null | undefined,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
if (!isEnabledRegistration(registrationInfo)) return;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Регистрация включена, поэтому запрос "нового" токена безопасен
|
|
44
|
+
await updatePushTokenAsync(await getDevicePushTokenAsync());
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn(
|
|
47
|
+
'[notifications] Error encountered while updating server registration with latest device push token.',
|
|
48
|
+
error,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let removeListener: (() => void) | null = null;
|
|
54
|
+
|
|
55
|
+
// Upstream ставит подписку при загрузке модуля, здесь явно, т.к. под `inlineRequires` barrel
|
|
56
|
+
// не гарантирует выполнение побочного эффекта
|
|
57
|
+
export function installPushTokenAutoRegistration(): () => void {
|
|
58
|
+
if (removeListener) return removeListener;
|
|
59
|
+
|
|
60
|
+
const getRegistrationInfoAsync =
|
|
61
|
+
serverRegistrationModule.getRegistrationInfoAsync;
|
|
62
|
+
if (!getRegistrationInfoAsync) {
|
|
63
|
+
console.warn(
|
|
64
|
+
'[notifications] Error encountered while fetching auto-registration state, new tokens will not be automatically registered on server.',
|
|
65
|
+
new UnavailabilityError(
|
|
66
|
+
'ServerRegistrationModule',
|
|
67
|
+
'getRegistrationInfoAsync',
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
return () => undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const subscription = addPushTokenListener(async token => {
|
|
74
|
+
try {
|
|
75
|
+
if (
|
|
76
|
+
isEnabledRegistration(
|
|
77
|
+
await serverRegistrationModule.getRegistrationInfoAsync?.(),
|
|
78
|
+
)
|
|
79
|
+
) {
|
|
80
|
+
await updatePushTokenAsync(token);
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn(
|
|
84
|
+
'[notifications] Error encountered while updating server registration with latest device push token.',
|
|
85
|
+
error,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
getRegistrationInfoAsync().then(
|
|
91
|
+
handlePersistedRegistrationInfoAsync,
|
|
92
|
+
error => {
|
|
93
|
+
console.error(
|
|
94
|
+
'[notifications] Error reading persisted server registration info: ',
|
|
95
|
+
error,
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
removeListener = () => {
|
|
101
|
+
subscription.remove();
|
|
102
|
+
removeListener = null;
|
|
103
|
+
};
|
|
104
|
+
return removeListener;
|
|
105
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Ported from expo-notifications sdk-57 utils/backoff.ts, itself from ide/backoff
|
|
2
|
+
export function computeNextBackoffInterval(
|
|
3
|
+
initialBackoff: number,
|
|
4
|
+
previousRetryCount: number,
|
|
5
|
+
{
|
|
6
|
+
multiplier = 1.5,
|
|
7
|
+
randomizationFactor = 0.25,
|
|
8
|
+
minBackoff = initialBackoff,
|
|
9
|
+
maxBackoff = Infinity,
|
|
10
|
+
} = {},
|
|
11
|
+
): number {
|
|
12
|
+
if (initialBackoff <= 0) initialBackoff = 1;
|
|
13
|
+
if (previousRetryCount < 0) previousRetryCount = 0;
|
|
14
|
+
if (multiplier < 1) multiplier = 1;
|
|
15
|
+
if (randomizationFactor < 0 || randomizationFactor > 1)
|
|
16
|
+
randomizationFactor = 0;
|
|
17
|
+
|
|
18
|
+
const nextBackoff = initialBackoff * multiplier ** previousRetryCount;
|
|
19
|
+
// Jitter within the negative to positive range of the randomization factor
|
|
20
|
+
const jitterFactor =
|
|
21
|
+
1 - randomizationFactor + 2 * randomizationFactor * Math.random();
|
|
22
|
+
|
|
23
|
+
return Math.min(Math.max(nextBackoff * jitterFactor, minBackoff), maxBackoff);
|
|
24
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Отдельно от `tokens.ts`, чтобы `auto-registration.ts` не зависел от него по кругу
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
UnavailabilityError,
|
|
5
|
+
type EventSubscription,
|
|
6
|
+
} from 'expo-modules-core';
|
|
7
|
+
|
|
8
|
+
import { dlog } from '@symbiote-native/engine';
|
|
9
|
+
|
|
10
|
+
import { pushTokenManager } from './native-modules';
|
|
11
|
+
import type { IDevicePushToken } from './types';
|
|
12
|
+
|
|
13
|
+
// Пакет только под iOS/Android, поэтому `Platform.OS` сужаем один раз здесь, а не кастами
|
|
14
|
+
function nativePlatform(): 'ios' | 'android' {
|
|
15
|
+
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
16
|
+
return Platform.OS;
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`[notifications] unsupported platform: ${Platform.OS}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type IPushTokenListener = (token: IDevicePushToken) => void;
|
|
22
|
+
|
|
23
|
+
/** Fires whenever the OS rolls the device push token while the app is running */
|
|
24
|
+
export function addPushTokenListener(
|
|
25
|
+
listener: IPushTokenListener,
|
|
26
|
+
): EventSubscription {
|
|
27
|
+
return pushTokenManager.addListener(
|
|
28
|
+
'onDevicePushToken',
|
|
29
|
+
({ devicePushToken }) => {
|
|
30
|
+
listener({ data: devicePushToken, type: nativePlatform() });
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let nativeTokenPromise: Promise<string> | null = null;
|
|
36
|
+
|
|
37
|
+
/** Returns the native FCM (Android) or APNs (iOS) device push token */
|
|
38
|
+
export async function getDevicePushTokenAsync(): Promise<IDevicePushToken> {
|
|
39
|
+
if (!pushTokenManager.getDevicePushTokenAsync) {
|
|
40
|
+
throw new UnavailabilityError('Notifications', 'getDevicePushTokenAsync');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let devicePushToken: string;
|
|
44
|
+
if (nativeTokenPromise) {
|
|
45
|
+
devicePushToken = await nativeTokenPromise;
|
|
46
|
+
} else {
|
|
47
|
+
nativeTokenPromise = pushTokenManager.getDevicePushTokenAsync();
|
|
48
|
+
devicePushToken = await nativeTokenPromise;
|
|
49
|
+
nativeTokenPromise = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
dlog('[notifications] getDevicePushTokenAsync resolved');
|
|
53
|
+
return { type: nativePlatform(), data: devicePushToken };
|
|
54
|
+
}
|
package/src/core/emitter.ts
CHANGED
|
@@ -87,6 +87,16 @@ export function clearLastNotificationResponse(): void {
|
|
|
87
87
|
emitter.emit(RESPONSE_CLEARED_EVENT, []);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** @deprecated Use `getLastNotificationResponse` instead */
|
|
91
|
+
export async function getLastNotificationResponseAsync(): Promise<INotificationResponse | null> {
|
|
92
|
+
return getLastNotificationResponse();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @deprecated Use `clearLastNotificationResponse` instead */
|
|
96
|
+
export async function clearLastNotificationResponseAsync(): Promise<void> {
|
|
97
|
+
clearLastNotificationResponse();
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
/** Fires whenever `clearLastNotificationResponse` runs. */
|
|
91
101
|
export function addNotificationResponseClearedListener(
|
|
92
102
|
listener: () => void,
|
package/src/core/index.ts
CHANGED
|
@@ -10,6 +10,10 @@ export {
|
|
|
10
10
|
unsubscribeFromTopicAsync,
|
|
11
11
|
type IPushTokenListener,
|
|
12
12
|
} from './tokens';
|
|
13
|
+
export {
|
|
14
|
+
installPushTokenAutoRegistration,
|
|
15
|
+
type IDevicePushTokenRegistration,
|
|
16
|
+
} from './auto-registration';
|
|
13
17
|
|
|
14
18
|
export {
|
|
15
19
|
dismissAllNotificationsAsync,
|
|
@@ -53,7 +57,9 @@ export {
|
|
|
53
57
|
addNotificationResponseReceivedListener,
|
|
54
58
|
addNotificationsDroppedListener,
|
|
55
59
|
clearLastNotificationResponse,
|
|
60
|
+
clearLastNotificationResponseAsync,
|
|
56
61
|
getLastNotificationResponse,
|
|
62
|
+
getLastNotificationResponseAsync,
|
|
57
63
|
} from './emitter';
|
|
58
64
|
|
|
59
65
|
export {
|
|
@@ -62,6 +68,8 @@ export {
|
|
|
62
68
|
unregisterTaskAsync,
|
|
63
69
|
} from './background-task';
|
|
64
70
|
|
|
71
|
+
export { determineNextResponse } from './last-notification-response';
|
|
72
|
+
|
|
65
73
|
export type {
|
|
66
74
|
IAudioAttributes,
|
|
67
75
|
IAudioAttributesInput,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Framework-agnostic twin of expo-notifications' `determineNextResponse` - every adapter's
|
|
2
|
+
// `useLastNotificationResponse` shares this dedup rule instead of re-deriving it
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
IMaybeNotificationResponse,
|
|
6
|
+
INotificationResponse,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
export function determineNextResponse(
|
|
10
|
+
prevResponse: IMaybeNotificationResponse,
|
|
11
|
+
newResponse: INotificationResponse | null,
|
|
12
|
+
): IMaybeNotificationResponse {
|
|
13
|
+
if (!newResponse) return null;
|
|
14
|
+
if (!prevResponse) return newResponse;
|
|
15
|
+
return prevResponse.notification.request.identifier !==
|
|
16
|
+
newResponse.notification.request.identifier
|
|
17
|
+
? newResponse
|
|
18
|
+
: prevResponse;
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Один активный запрос ресинка токена, новый прерывает предыдущий
|
|
2
|
+
let lastAbortController: AbortController | null = null;
|
|
3
|
+
|
|
4
|
+
export function abortPendingRegistration(): void {
|
|
5
|
+
lastAbortController?.abort();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function startAbortableRegistration(): AbortSignal {
|
|
9
|
+
abortPendingRegistration();
|
|
10
|
+
lastAbortController = new AbortController();
|
|
11
|
+
return lastAbortController.signal;
|
|
12
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getIosPushNotificationServiceEnvironmentAsync } from '@symbiote-native/application';
|
|
2
|
+
import { CodedError, Platform, UnavailabilityError } from 'expo-modules-core';
|
|
3
|
+
|
|
4
|
+
import { serverRegistrationModule } from './native-modules';
|
|
5
|
+
import type { IDevicePushToken } from './types';
|
|
6
|
+
|
|
7
|
+
export async function getDeviceIdAsync(): Promise<string> {
|
|
8
|
+
try {
|
|
9
|
+
if (!serverRegistrationModule.getInstallationIdAsync) {
|
|
10
|
+
throw new UnavailabilityError('Notifications', 'getInstallationIdAsync');
|
|
11
|
+
}
|
|
12
|
+
return await serverRegistrationModule.getInstallationIdAsync();
|
|
13
|
+
} catch (error) {
|
|
14
|
+
throw new CodedError(
|
|
15
|
+
'ERR_NOTIF_DEVICE_ID',
|
|
16
|
+
`Could not fetch the installation ID of the application: ${error}.`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getTypeOfToken(devicePushToken: IDevicePushToken): string {
|
|
22
|
+
switch (devicePushToken.type) {
|
|
23
|
+
case 'ios':
|
|
24
|
+
return 'apns';
|
|
25
|
+
case 'android':
|
|
26
|
+
return 'fcm';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function shouldUseDevelopmentNotificationService(): Promise<boolean> {
|
|
31
|
+
if (Platform.OS === 'ios') {
|
|
32
|
+
try {
|
|
33
|
+
return (
|
|
34
|
+
(await getIosPushNotificationServiceEnvironmentAsync()) ===
|
|
35
|
+
'development'
|
|
36
|
+
);
|
|
37
|
+
} catch {
|
|
38
|
+
// Не смогли узнать окружение, считаем что не development
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|