react-native-qpush 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +439 -2
- package/android/build.gradle +77 -0
- package/android/gradle.properties +5 -0
- package/android/proguard-rules.pro +5 -0
- package/android/src/main/AndroidManifest.xml +29 -0
- package/android/src/main/java/com/qpush/reactnative/QPushDeviceInfo.kt +96 -0
- package/android/src/main/java/com/qpush/reactnative/QPushEventBus.kt +153 -0
- package/android/src/main/java/com/qpush/reactnative/QPushFcm.kt +108 -0
- package/android/src/main/java/com/qpush/reactnative/QPushHttp.kt +92 -0
- package/android/src/main/java/com/qpush/reactnative/QPushImages.kt +103 -0
- package/android/src/main/java/com/qpush/reactnative/QPushLog.kt +39 -0
- package/android/src/main/java/com/qpush/reactnative/QPushMessagingService.kt +77 -0
- package/android/src/main/java/com/qpush/reactnative/QPushNotificationOpenActivity.kt +121 -0
- package/android/src/main/java/com/qpush/reactnative/QPushNotificationRenderer.kt +256 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPayload.kt +114 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPushModule.kt +368 -0
- package/android/src/main/java/com/qpush/reactnative/QPushPushPackage.kt +15 -0
- package/android/src/main/java/com/qpush/reactnative/QPushStore.kt +108 -0
- package/android/src/main/res/values/qpush_strings.xml +6 -0
- package/app.plugin.js +1 -0
- package/ios/Extension/QPushNotificationServiceExtension.swift +261 -0
- package/ios/Extension/QPushPushExtension.podspec +34 -0
- package/ios/QPushPush.m +62 -0
- package/ios/QPushPush.swift +186 -0
- package/ios/QPushPushBridge.h +5 -0
- package/ios/QPushPushCore.swift +457 -0
- package/ios/QPushPushHooks.m +237 -0
- package/ios/QPushPushSupport.swift +249 -0
- package/ios_nse_template/Info.plist +38 -0
- package/ios_nse_template/NotificationService.entitlements +10 -0
- package/ios_nse_template/NotificationService.swift +44 -0
- package/lib/NativeQPushPush.d.ts +44 -0
- package/lib/NativeQPushPush.js +4 -0
- package/lib/QPush.d.ts +117 -0
- package/lib/QPush.js +206 -0
- package/lib/core/QPushCore.d.ts +148 -0
- package/lib/core/QPushCore.js +1387 -0
- package/lib/core/constants.d.ts +37 -0
- package/lib/core/constants.js +40 -0
- package/lib/core/events.d.ts +11 -0
- package/lib/core/events.js +40 -0
- package/lib/core/http.d.ts +41 -0
- package/lib/core/http.js +123 -0
- package/lib/core/logger.d.ts +18 -0
- package/lib/core/logger.js +43 -0
- package/lib/core/operationQueue.d.ts +117 -0
- package/lib/core/operationQueue.js +264 -0
- package/lib/core/operations.d.ts +80 -0
- package/lib/core/operations.js +88 -0
- package/lib/core/payload.d.ts +20 -0
- package/lib/core/payload.js +187 -0
- package/lib/core/platform.d.ts +72 -0
- package/lib/core/platform.js +7 -0
- package/lib/core/state.d.ts +51 -0
- package/lib/core/state.js +74 -0
- package/lib/core/storage.d.ts +34 -0
- package/lib/core/storage.js +68 -0
- package/lib/core/subscription.d.ts +67 -0
- package/lib/core/subscription.js +98 -0
- package/lib/core/tags.d.ts +39 -0
- package/lib/core/tags.js +123 -0
- package/lib/core/url.d.ts +17 -0
- package/lib/core/url.js +64 -0
- package/lib/core/uuid.d.ts +2 -0
- package/lib/core/uuid.js +18 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +15 -0
- package/lib/notification.d.ts +67 -0
- package/lib/notification.js +73 -0
- package/lib/reactNativePlatform.d.ts +3 -0
- package/lib/reactNativePlatform.js +164 -0
- package/lib/types.d.ts +157 -0
- package/lib/types.js +31 -0
- package/package.json +70 -4
- package/plugin/withQPush.js +330 -0
- package/react-native-qpush.podspec +30 -0
- package/react-native.config.js +18 -0
- package/src/NativeQPushPush.ts +57 -0
- package/src/QPush.ts +292 -0
- package/src/core/QPushCore.ts +1515 -0
- package/src/core/constants.ts +42 -0
- package/src/core/events.ts +34 -0
- package/src/core/http.ts +146 -0
- package/src/core/logger.ts +42 -0
- package/src/core/operationQueue.ts +324 -0
- package/src/core/operations.ts +166 -0
- package/src/core/payload.ts +175 -0
- package/src/core/platform.ts +84 -0
- package/src/core/state.ts +127 -0
- package/src/core/storage.ts +76 -0
- package/src/core/subscription.ts +133 -0
- package/src/core/tags.ts +116 -0
- package/src/core/url.ts +58 -0
- package/src/core/uuid.ts +13 -0
- package/src/index.ts +23 -0
- package/src/notification.ts +113 -0
- package/src/reactNativePlatform.ts +172 -0
- package/src/types.ts +174 -0
|
@@ -0,0 +1,1387 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QPushCore = void 0;
|
|
4
|
+
exports.languageFromLocale = languageFromLocale;
|
|
5
|
+
const notification_1 = require("../notification");
|
|
6
|
+
const types_1 = require("../types");
|
|
7
|
+
const constants_1 = require("./constants");
|
|
8
|
+
const events_1 = require("./events");
|
|
9
|
+
const http_1 = require("./http");
|
|
10
|
+
const logger_1 = require("./logger");
|
|
11
|
+
const operationQueue_1 = require("./operationQueue");
|
|
12
|
+
const operations_1 = require("./operations");
|
|
13
|
+
const payload_1 = require("./payload");
|
|
14
|
+
const state_1 = require("./state");
|
|
15
|
+
const storage_1 = require("./storage");
|
|
16
|
+
const subscription_1 = require("./subscription");
|
|
17
|
+
const tags_1 = require("./tags");
|
|
18
|
+
const url_1 = require("./url");
|
|
19
|
+
const uuid_1 = require("./uuid");
|
|
20
|
+
/** Aliases that cannot be set / removed through `addAlias` / `removeAlias`. */
|
|
21
|
+
const RESERVED_ALIASES = ['qpush_id', 'onesignal_id', 'external_id'];
|
|
22
|
+
function identityQPushId(identity) {
|
|
23
|
+
var _a;
|
|
24
|
+
if (!identity)
|
|
25
|
+
return undefined;
|
|
26
|
+
const id = (_a = identity.qpush_id) !== null && _a !== void 0 ? _a : identity.onesignal_id;
|
|
27
|
+
return typeof id === 'string' && id.length > 0 ? id : undefined;
|
|
28
|
+
}
|
|
29
|
+
function aliasesFromIdentity(identity) {
|
|
30
|
+
const aliases = {};
|
|
31
|
+
for (const key of Object.keys(identity)) {
|
|
32
|
+
const value = identity[key];
|
|
33
|
+
if (key === 'qpush_id' || key === 'onesignal_id')
|
|
34
|
+
continue;
|
|
35
|
+
if (typeof value === 'string')
|
|
36
|
+
aliases[key] = value;
|
|
37
|
+
}
|
|
38
|
+
return aliases;
|
|
39
|
+
}
|
|
40
|
+
function stringTags(tags) {
|
|
41
|
+
const result = {};
|
|
42
|
+
if (typeof tags !== 'object' || tags === null)
|
|
43
|
+
return result;
|
|
44
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
45
|
+
if (value === null || value === undefined || value === '')
|
|
46
|
+
continue;
|
|
47
|
+
result[key] = typeof value === 'string' ? value : String(value);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function samePushState(a, b) {
|
|
52
|
+
return a.id === b.id && a.token === b.token && a.optedIn === b.optedIn;
|
|
53
|
+
}
|
|
54
|
+
function permissionToEnum(status) {
|
|
55
|
+
switch (status) {
|
|
56
|
+
case 'granted':
|
|
57
|
+
return types_1.NotificationPermission.Authorized;
|
|
58
|
+
case 'denied':
|
|
59
|
+
return types_1.NotificationPermission.Denied;
|
|
60
|
+
case 'provisional':
|
|
61
|
+
return types_1.NotificationPermission.Provisional;
|
|
62
|
+
case 'ephemeral':
|
|
63
|
+
return types_1.NotificationPermission.Ephemeral;
|
|
64
|
+
default:
|
|
65
|
+
return types_1.NotificationPermission.NotDetermined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Language code from a locale (`pt-BR` -> `pt`, `iw` -> `he`). */
|
|
69
|
+
function languageFromLocale(locale) {
|
|
70
|
+
var _a, _b;
|
|
71
|
+
if (!locale)
|
|
72
|
+
return undefined;
|
|
73
|
+
const code = (_a = locale.split(/[-_]/)[0]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
74
|
+
if (!code)
|
|
75
|
+
return undefined;
|
|
76
|
+
const legacy = { iw: 'he', in: 'id', ji: 'yi' };
|
|
77
|
+
return (_b = legacy[code]) !== null && _b !== void 0 ? _b : code;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The single implementation of the QPush user / subscription / HTTP state
|
|
81
|
+
* machine. Platform specifics are injected through {@link PlatformAdapter}.
|
|
82
|
+
*/
|
|
83
|
+
class QPushCore {
|
|
84
|
+
constructor(platform, logger = new logger_1.Logger()) {
|
|
85
|
+
this.platform = platform;
|
|
86
|
+
this.events = new events_1.EventHub();
|
|
87
|
+
this.loadPromise = null;
|
|
88
|
+
this.commandChain = Promise.resolve();
|
|
89
|
+
this.device = null;
|
|
90
|
+
this.initializeCalled = false;
|
|
91
|
+
this.started = false;
|
|
92
|
+
this.registrationSettled = false;
|
|
93
|
+
this.registering = false;
|
|
94
|
+
this.halted = false;
|
|
95
|
+
this.foregroundAt = null;
|
|
96
|
+
this.backgroundAt = null;
|
|
97
|
+
this.pendingColdSession = false;
|
|
98
|
+
this.appStateSubscription = null;
|
|
99
|
+
this.nativeSubscriptions = [];
|
|
100
|
+
this.pendingClicks = [];
|
|
101
|
+
this.clickFlushScheduled = false;
|
|
102
|
+
this.logger = logger;
|
|
103
|
+
this.state = (0, state_1.createDefaultState)(this.uuid());
|
|
104
|
+
this.stateWriter = new storage_1.SerialWriter(platform.native, constants_1.StorageKeys.state, (error) => this.logger.error('Failed to persist state', error));
|
|
105
|
+
this.http = new http_1.HttpClient({
|
|
106
|
+
fetch: platform.fetch,
|
|
107
|
+
getBaseUrl: () => { var _a; return (_a = this.state.config) === null || _a === void 0 ? void 0 : _a.serverUrl; },
|
|
108
|
+
getSubscriptionId: () => this.state.subscription.id,
|
|
109
|
+
scheduler: platform.scheduler,
|
|
110
|
+
logger: this.logger,
|
|
111
|
+
now: () => platform.now(),
|
|
112
|
+
});
|
|
113
|
+
this.queue = new operationQueue_1.OperationQueue({
|
|
114
|
+
store: platform.native,
|
|
115
|
+
storageKey: constants_1.StorageKeys.queue,
|
|
116
|
+
execute: (operation, item) => this.execute(operation, item),
|
|
117
|
+
canProcess: () => this.canProcess(),
|
|
118
|
+
scheduler: platform.scheduler,
|
|
119
|
+
now: () => platform.now(),
|
|
120
|
+
random: () => platform.random(),
|
|
121
|
+
logger: this.logger,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Plumbing
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
uuid() {
|
|
128
|
+
return (0, uuid_1.generateUuid)(() => this.platform.random());
|
|
129
|
+
}
|
|
130
|
+
ensureLoaded() {
|
|
131
|
+
if (!this.loadPromise)
|
|
132
|
+
this.loadPromise = this.load();
|
|
133
|
+
return this.loadPromise;
|
|
134
|
+
}
|
|
135
|
+
async load() {
|
|
136
|
+
const saved = await (0, storage_1.readJson)(this.platform.native, constants_1.StorageKeys.state);
|
|
137
|
+
this.state = (0, state_1.sanitizeState)(saved, () => this.uuid());
|
|
138
|
+
await this.queue.load();
|
|
139
|
+
// Must be enqueued before any operation of this user (tags set before
|
|
140
|
+
// initialize, consent, ...). Execution waits until the SDK started.
|
|
141
|
+
this.ensureUserCreation();
|
|
142
|
+
}
|
|
143
|
+
/** Enqueues `createUser` for the current user unless it exists or is being created. */
|
|
144
|
+
ensureUserCreation() {
|
|
145
|
+
const user = this.state.user;
|
|
146
|
+
if (user.qpushId)
|
|
147
|
+
return;
|
|
148
|
+
const pending = this.queue.has((operation) => (operation.kind === 'createUser' || operation.kind === 'login') && operation.userLocalId === user.localId);
|
|
149
|
+
if (pending)
|
|
150
|
+
return;
|
|
151
|
+
this.queue.enqueue({
|
|
152
|
+
kind: 'createUser',
|
|
153
|
+
userLocalId: user.localId,
|
|
154
|
+
identity: user.externalId ? { external_id: user.externalId } : {},
|
|
155
|
+
transferSubscription: !!this.state.subscription.id,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** Runs `fn` after state is loaded, serialized with every other command. */
|
|
159
|
+
command(fn) {
|
|
160
|
+
const next = this.commandChain
|
|
161
|
+
.then(() => this.ensureLoaded())
|
|
162
|
+
.then(fn)
|
|
163
|
+
.catch((error) => this.logger.error('Unexpected error', error));
|
|
164
|
+
this.commandChain = next;
|
|
165
|
+
return next;
|
|
166
|
+
}
|
|
167
|
+
/** Reads state after every previously issued command ran. */
|
|
168
|
+
async read(fn) {
|
|
169
|
+
let result;
|
|
170
|
+
await this.command(() => {
|
|
171
|
+
result = fn();
|
|
172
|
+
});
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
save() {
|
|
176
|
+
void this.stateWriter.write(JSON.stringify(this.state));
|
|
177
|
+
}
|
|
178
|
+
canProcess() {
|
|
179
|
+
return this.started && this.registrationSettled && !this.halted && this.hasConsent();
|
|
180
|
+
}
|
|
181
|
+
hasConsent() {
|
|
182
|
+
return !this.state.consentRequired || this.state.consentGiven;
|
|
183
|
+
}
|
|
184
|
+
sleep(ms) {
|
|
185
|
+
return new Promise((resolve) => this.platform.scheduler.setTimeout(resolve, ms));
|
|
186
|
+
}
|
|
187
|
+
get appId() {
|
|
188
|
+
var _a, _b;
|
|
189
|
+
return (0, url_1.encodeSegment)((_b = (_a = this.state.config) === null || _a === void 0 ? void 0 : _a.appId) !== null && _b !== void 0 ? _b : '');
|
|
190
|
+
}
|
|
191
|
+
/** Waits for pending commands, queue processing and storage writes (tests). */
|
|
192
|
+
async flush() {
|
|
193
|
+
await this.commandChain;
|
|
194
|
+
await this.queue.process();
|
|
195
|
+
await this.queue.idle();
|
|
196
|
+
await this.commandChain;
|
|
197
|
+
await this.queue.persisted();
|
|
198
|
+
await this.stateWriter.flush();
|
|
199
|
+
}
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Initialization & consent
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
setLogLevel(level) {
|
|
204
|
+
this.logger.setLevel(level);
|
|
205
|
+
try {
|
|
206
|
+
this.platform.native.setLogLevel(level);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// ignore
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
initialize(appId, options) {
|
|
213
|
+
var _a;
|
|
214
|
+
if (options && typeof options.logLevel === 'number')
|
|
215
|
+
this.setLogLevel(options.logLevel);
|
|
216
|
+
if (typeof appId !== 'string' || appId.trim() === '') {
|
|
217
|
+
this.logger.error('initialize: appId is required');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const serverUrl = (0, url_1.normalizeServerUrl)((_a = options === null || options === void 0 ? void 0 : options.serverUrl) !== null && _a !== void 0 ? _a : types_1.QPUSH_CLOUD_API_URL);
|
|
221
|
+
if (!serverUrl) {
|
|
222
|
+
this.logger.error(`initialize: invalid serverUrl "${String(options === null || options === void 0 ? void 0 : options.serverUrl)}"`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const trimmedAppId = appId.trim();
|
|
226
|
+
if (serverUrl.startsWith('http://')) {
|
|
227
|
+
this.logger.warn('initialize: serverUrl uses plain HTTP. Use HTTPS in production. Release builds on Android block ' +
|
|
228
|
+
'cleartext traffic (delivery receipts and click reports are sent by native code); on iOS use an IP ' +
|
|
229
|
+
'address, or NSAllowsLocalNetworking for .local hostnames.');
|
|
230
|
+
}
|
|
231
|
+
this.initializeCalled = true;
|
|
232
|
+
this.attachNativeEvents();
|
|
233
|
+
void this.command(async () => {
|
|
234
|
+
var _a;
|
|
235
|
+
const previous = this.state.config;
|
|
236
|
+
if (previous && previous.appId === trimmedAppId && previous.serverUrl === serverUrl && this.started) {
|
|
237
|
+
this.logger.debug('initialize: already initialized');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (previous && (previous.appId !== trimmedAppId || previous.serverUrl !== serverUrl)) {
|
|
241
|
+
this.logger.info('initialize: app id or server changed, resetting local user and subscription');
|
|
242
|
+
this.resetForNewApp();
|
|
243
|
+
}
|
|
244
|
+
this.state.config = { appId: trimmedAppId, serverUrl };
|
|
245
|
+
this.save();
|
|
246
|
+
await this.writeNative(constants_1.NativeKeys.appId, trimmedAppId);
|
|
247
|
+
await this.writeNative(constants_1.NativeKeys.serverUrl, serverUrl);
|
|
248
|
+
await this.writeNative(constants_1.NativeKeys.subscriptionId, (_a = this.state.subscription.id) !== null && _a !== void 0 ? _a : null);
|
|
249
|
+
this.maybeStart();
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
resetForNewApp() {
|
|
253
|
+
const { consentRequired, consentGiven } = this.state;
|
|
254
|
+
const subscription = this.state.subscription;
|
|
255
|
+
this.queue.clear();
|
|
256
|
+
this.state = (0, state_1.createDefaultState)(this.uuid());
|
|
257
|
+
this.state.consentRequired = consentRequired;
|
|
258
|
+
this.state.consentGiven = consentGiven;
|
|
259
|
+
// Keep what belongs to the device, drop what belongs to the old app.
|
|
260
|
+
this.state.subscription = {
|
|
261
|
+
optedIn: subscription.optedIn,
|
|
262
|
+
permission: subscription.permission,
|
|
263
|
+
token: subscription.token,
|
|
264
|
+
tokenError: subscription.tokenError,
|
|
265
|
+
};
|
|
266
|
+
this.started = false;
|
|
267
|
+
this.registrationSettled = false;
|
|
268
|
+
this.halted = false;
|
|
269
|
+
this.ensureUserCreation();
|
|
270
|
+
}
|
|
271
|
+
async writeNative(key, value) {
|
|
272
|
+
try {
|
|
273
|
+
if (value === null)
|
|
274
|
+
await this.platform.native.removeItem(key);
|
|
275
|
+
else
|
|
276
|
+
await this.platform.native.setItem(key, value);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
this.logger.warn(`Could not persist ${key} natively`, error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
setConsentRequired(required) {
|
|
283
|
+
void this.command(() => {
|
|
284
|
+
this.state.consentRequired = !!required;
|
|
285
|
+
this.save();
|
|
286
|
+
this.maybeStart();
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
setConsentGiven(given) {
|
|
290
|
+
void this.command(() => {
|
|
291
|
+
this.state.consentGiven = !!given;
|
|
292
|
+
this.save();
|
|
293
|
+
this.maybeStart();
|
|
294
|
+
if (this.canProcess())
|
|
295
|
+
this.queue.kick();
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
maybeStart() {
|
|
299
|
+
if (!this.state.config || this.started)
|
|
300
|
+
return;
|
|
301
|
+
if (!this.hasConsent()) {
|
|
302
|
+
this.logger.info('Waiting for privacy consent (setConsentGiven(true)) before starting');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.started = true;
|
|
306
|
+
void this.start().catch((error) => this.logger.error('Start failed', error));
|
|
307
|
+
}
|
|
308
|
+
async start() {
|
|
309
|
+
try {
|
|
310
|
+
this.device = await this.platform.native.getDeviceInfo();
|
|
311
|
+
if (this.device && !this.device.language)
|
|
312
|
+
this.device.language = languageFromLocale(this.device.locale);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
this.logger.warn('Could not read device info', error);
|
|
316
|
+
this.device = { platform: this.platform.os };
|
|
317
|
+
}
|
|
318
|
+
await this.refreshPermission(true);
|
|
319
|
+
await this.command(() => {
|
|
320
|
+
this.ensureUserCreation();
|
|
321
|
+
// Pushes changed device fields (app version, OS version...) if any.
|
|
322
|
+
this.mutateSubscription(() => { });
|
|
323
|
+
});
|
|
324
|
+
this.attachAppState();
|
|
325
|
+
const appState = this.platform.appState.currentState;
|
|
326
|
+
if (appState === 'active' || appState == null || appState === 'unknown') {
|
|
327
|
+
await this.command(() => this.startSession());
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
this.pendingColdSession = true;
|
|
331
|
+
}
|
|
332
|
+
await this.command(() => {
|
|
333
|
+
if (this.state.user.qpushId) {
|
|
334
|
+
this.queue.enqueue({ kind: 'fetchUser', userLocalId: this.state.user.localId });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
await this.registerPush();
|
|
338
|
+
this.registrationSettled = true;
|
|
339
|
+
this.queue.kick();
|
|
340
|
+
}
|
|
341
|
+
/** Retries push registration when the app comes back without a token (offline start, FCM params added later). */
|
|
342
|
+
retryRegistrationIfNeeded() {
|
|
343
|
+
const sub = this.state.subscription;
|
|
344
|
+
if (!this.registrationSettled || this.registering || this.halted || sub.token)
|
|
345
|
+
return;
|
|
346
|
+
this.registering = true;
|
|
347
|
+
void this.registerPush()
|
|
348
|
+
.catch((error) => this.logger.warn('Push registration retry failed', error))
|
|
349
|
+
.finally(() => {
|
|
350
|
+
this.registering = false;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Push registration & permission
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
async fetchParams() {
|
|
357
|
+
var _a;
|
|
358
|
+
const file = this.platform.os === 'android' ? 'android_params.js' : 'ios_params.js';
|
|
359
|
+
const path = `/apps/${this.appId}/${file}`;
|
|
360
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
361
|
+
const response = await this.http.request('GET', path);
|
|
362
|
+
if (response.ok) {
|
|
363
|
+
if (this.platform.os === 'android') {
|
|
364
|
+
const fcm = response.data && 'fcm' in response.data ? (_a = response.data.fcm) !== null && _a !== void 0 ? _a : null : null;
|
|
365
|
+
await this.command(() => {
|
|
366
|
+
this.state.fcmParams = fcm;
|
|
367
|
+
this.save();
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return 'ok';
|
|
371
|
+
}
|
|
372
|
+
if (response.status === 404)
|
|
373
|
+
return 'not_found';
|
|
374
|
+
if (!(0, http_1.isRetryableStatus)(response.status))
|
|
375
|
+
return 'unavailable';
|
|
376
|
+
if (attempt < 3)
|
|
377
|
+
await this.sleep(1000 * Math.pow(2, attempt - 1));
|
|
378
|
+
}
|
|
379
|
+
return 'unavailable';
|
|
380
|
+
}
|
|
381
|
+
async registerPush() {
|
|
382
|
+
var _a, _b, _c;
|
|
383
|
+
const params = await this.fetchParams();
|
|
384
|
+
if (params === 'not_found') {
|
|
385
|
+
this.halted = true;
|
|
386
|
+
this.logger.error(`App "${(_a = this.state.config) === null || _a === void 0 ? void 0 : _a.appId}" was not found on ${(_b = this.state.config) === null || _b === void 0 ? void 0 : _b.serverUrl}. ` +
|
|
387
|
+
'Check the app id and serverUrl; the SDK will not sync until the app restarts.');
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (params === 'unavailable')
|
|
391
|
+
this.logger.warn('Could not fetch app params, using cached values');
|
|
392
|
+
const fcm = this.platform.os === 'android' ? (_c = this.state.fcmParams) !== null && _c !== void 0 ? _c : null : null;
|
|
393
|
+
let registration;
|
|
394
|
+
try {
|
|
395
|
+
registration = this.platform.native.registerForPush(fcm).then((result) => this.applyRegistration(result), (error) => this.logger.warn('Push registration failed', error));
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
this.logger.warn('Push registration failed', error);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
let timer;
|
|
402
|
+
const timeout = new Promise((resolve) => {
|
|
403
|
+
timer = this.platform.scheduler.setTimeout(() => {
|
|
404
|
+
this.logger.warn('Push token not received yet, continuing without it');
|
|
405
|
+
resolve();
|
|
406
|
+
}, constants_1.PUSH_REGISTRATION_TIMEOUT_MS);
|
|
407
|
+
});
|
|
408
|
+
await Promise.race([registration, timeout]);
|
|
409
|
+
this.platform.scheduler.clearTimeout(timer);
|
|
410
|
+
}
|
|
411
|
+
applyRegistration(result) {
|
|
412
|
+
return this.command(() => {
|
|
413
|
+
if (!result)
|
|
414
|
+
return;
|
|
415
|
+
this.mutateSubscription(() => {
|
|
416
|
+
var _a;
|
|
417
|
+
const sub = this.state.subscription;
|
|
418
|
+
if (result.token) {
|
|
419
|
+
sub.token = result.token;
|
|
420
|
+
sub.tokenError = undefined;
|
|
421
|
+
}
|
|
422
|
+
else if (typeof result.notificationTypes === 'number' && result.notificationTypes < 0) {
|
|
423
|
+
// A transient failure (e.g. offline) must not unsubscribe a device
|
|
424
|
+
// that already has a valid token.
|
|
425
|
+
if (!sub.token)
|
|
426
|
+
sub.tokenError = result.notificationTypes;
|
|
427
|
+
this.logger.warn(`Push registration error (${result.notificationTypes}): ${(_a = result.error) !== null && _a !== void 0 ? _a : 'unknown'}`);
|
|
428
|
+
}
|
|
429
|
+
else if (result.error) {
|
|
430
|
+
this.logger.warn(`Push registration error: ${result.error}`);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
applyToken(token) {
|
|
436
|
+
if (!token)
|
|
437
|
+
return;
|
|
438
|
+
void this.command(() => {
|
|
439
|
+
this.mutateSubscription(() => {
|
|
440
|
+
this.state.subscription.token = token;
|
|
441
|
+
this.state.subscription.tokenError = undefined;
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
/** Reads the OS permission and updates the subscription when it changed. */
|
|
446
|
+
async refreshPermission(emit) {
|
|
447
|
+
let status;
|
|
448
|
+
try {
|
|
449
|
+
status = await this.platform.native.getPermissionStatus();
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
this.logger.warn('Could not read notification permission', error);
|
|
453
|
+
return this.read(() => this.state.subscription.permission);
|
|
454
|
+
}
|
|
455
|
+
await this.command(() => {
|
|
456
|
+
const previous = this.state.subscription.permission;
|
|
457
|
+
if (previous === status)
|
|
458
|
+
return;
|
|
459
|
+
this.mutateSubscription(() => {
|
|
460
|
+
this.state.subscription.permission = status;
|
|
461
|
+
});
|
|
462
|
+
const wasGranted = (0, subscription_1.isPermissionGranted)(previous);
|
|
463
|
+
const isGranted = (0, subscription_1.isPermissionGranted)(status);
|
|
464
|
+
if (emit && wasGranted !== isGranted) {
|
|
465
|
+
this.events.emit('permissionChange', isGranted, (error) => this.logger.error('permissionChange listener threw', error));
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
return status;
|
|
469
|
+
}
|
|
470
|
+
async requestPermission(fallbackToSettings) {
|
|
471
|
+
await this.ensureLoaded();
|
|
472
|
+
let granted = false;
|
|
473
|
+
try {
|
|
474
|
+
granted = await this.platform.native.requestPermission(!!fallbackToSettings);
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
this.logger.error('requestPermission failed', error);
|
|
478
|
+
}
|
|
479
|
+
const status = await this.refreshPermission(true);
|
|
480
|
+
return granted || (0, subscription_1.isPermissionGranted)(status);
|
|
481
|
+
}
|
|
482
|
+
async registerForProvisionalAuthorization() {
|
|
483
|
+
await this.ensureLoaded();
|
|
484
|
+
let accepted = false;
|
|
485
|
+
try {
|
|
486
|
+
accepted = await this.platform.native.registerForProvisionalAuthorization();
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
this.logger.error('registerForProvisionalAuthorization failed', error);
|
|
490
|
+
}
|
|
491
|
+
await this.refreshPermission(true);
|
|
492
|
+
return accepted;
|
|
493
|
+
}
|
|
494
|
+
async getPermission() {
|
|
495
|
+
await this.ensureLoaded();
|
|
496
|
+
return (0, subscription_1.isPermissionGranted)(await this.refreshPermission(true));
|
|
497
|
+
}
|
|
498
|
+
async permissionNative() {
|
|
499
|
+
await this.ensureLoaded();
|
|
500
|
+
return permissionToEnum(await this.refreshPermission(true));
|
|
501
|
+
}
|
|
502
|
+
async canRequestPermission() {
|
|
503
|
+
try {
|
|
504
|
+
return await this.platform.native.canRequestPermission();
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
// Subscription
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
subscriptionInputs() {
|
|
514
|
+
const sub = this.state.subscription;
|
|
515
|
+
return { token: sub.token, tokenError: sub.tokenError, permission: sub.permission, optedIn: sub.optedIn };
|
|
516
|
+
}
|
|
517
|
+
currentFields() {
|
|
518
|
+
return (0, subscription_1.buildSubscriptionFields)(this.subscriptionInputs(), this.device);
|
|
519
|
+
}
|
|
520
|
+
pushState() {
|
|
521
|
+
var _a, _b;
|
|
522
|
+
const sub = this.state.subscription;
|
|
523
|
+
return { id: (_a = sub.id) !== null && _a !== void 0 ? _a : null, token: (_b = sub.token) !== null && _b !== void 0 ? _b : null, optedIn: (0, subscription_1.computeOptedIn)(this.subscriptionInputs()) };
|
|
524
|
+
}
|
|
525
|
+
/** Applies a subscription change, emits `change` and schedules a sync. */
|
|
526
|
+
mutateSubscription(fn) {
|
|
527
|
+
const before = this.pushState();
|
|
528
|
+
fn();
|
|
529
|
+
const after = this.pushState();
|
|
530
|
+
this.save();
|
|
531
|
+
this.scheduleSyncIfDirty();
|
|
532
|
+
if (!samePushState(before, after)) {
|
|
533
|
+
this.events.emit('pushSubscriptionChange', { previous: before, current: after }, (error) => this.logger.error('pushSubscription change listener threw', error));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
scheduleSyncIfDirty() {
|
|
537
|
+
const sub = this.state.subscription;
|
|
538
|
+
// A pending create sends the live subscription snapshot anyway.
|
|
539
|
+
if (!sub.id && this.queue.has((operation, item) => item.id !== this.queue.inFlight && (0, operations_1.createsSubscription)(operation))) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const dirty = sub.badgeResetPending || !(0, tags_1.isEmpty)((0, subscription_1.diffSubscription)(sub.synced, this.currentFields()));
|
|
543
|
+
if (dirty)
|
|
544
|
+
this.queue.enqueue({ kind: 'syncSubscription' });
|
|
545
|
+
}
|
|
546
|
+
setSubscriptionId(id) {
|
|
547
|
+
if (this.state.subscription.id === id)
|
|
548
|
+
return;
|
|
549
|
+
this.mutateSubscription(() => {
|
|
550
|
+
this.state.subscription.id = id;
|
|
551
|
+
});
|
|
552
|
+
void this.writeNative(constants_1.NativeKeys.subscriptionId, id !== null && id !== void 0 ? id : null);
|
|
553
|
+
}
|
|
554
|
+
optIn() {
|
|
555
|
+
void this.command(async () => {
|
|
556
|
+
this.mutateSubscription(() => {
|
|
557
|
+
this.state.subscription.optedIn = true;
|
|
558
|
+
});
|
|
559
|
+
if (this.state.subscription.permission === 'notDetermined') {
|
|
560
|
+
void this.requestPermission(false);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
optOut() {
|
|
565
|
+
void this.command(() => {
|
|
566
|
+
this.mutateSubscription(() => {
|
|
567
|
+
this.state.subscription.optedIn = false;
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
getSubscriptionId() {
|
|
572
|
+
return this.read(() => { var _a; return (_a = this.state.subscription.id) !== null && _a !== void 0 ? _a : null; });
|
|
573
|
+
}
|
|
574
|
+
getSubscriptionToken() {
|
|
575
|
+
return this.read(() => { var _a; return (_a = this.state.subscription.token) !== null && _a !== void 0 ? _a : null; });
|
|
576
|
+
}
|
|
577
|
+
getOptedIn() {
|
|
578
|
+
return this.read(() => (0, subscription_1.computeOptedIn)(this.subscriptionInputs()));
|
|
579
|
+
}
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
// User
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
userState() {
|
|
584
|
+
var _a, _b;
|
|
585
|
+
return { qpushId: (_a = this.state.user.qpushId) !== null && _a !== void 0 ? _a : null, externalId: (_b = this.state.user.externalId) !== null && _b !== void 0 ? _b : null };
|
|
586
|
+
}
|
|
587
|
+
mutateUser(fn) {
|
|
588
|
+
const before = this.userState();
|
|
589
|
+
fn();
|
|
590
|
+
const after = this.userState();
|
|
591
|
+
this.save();
|
|
592
|
+
if (before.qpushId !== after.qpushId || before.externalId !== after.externalId) {
|
|
593
|
+
this.events.emit('userChange', { current: after }, (error) => this.logger.error('User change listener threw', error));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
login(externalId) {
|
|
597
|
+
void this.command(() => {
|
|
598
|
+
if (typeof externalId !== 'string' || externalId.trim() === '') {
|
|
599
|
+
this.logger.error('login: externalId must be a non-empty string');
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const previous = this.state.user;
|
|
603
|
+
if (previous.externalId === externalId) {
|
|
604
|
+
this.logger.debug('login: already logged in with this external id');
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const anonymous = !previous.externalId;
|
|
608
|
+
const next = {
|
|
609
|
+
...(0, state_1.createUser)(this.uuid()),
|
|
610
|
+
externalId,
|
|
611
|
+
// Identifying an anonymous user keeps its data (OneSignal semantics).
|
|
612
|
+
aliases: anonymous ? { ...previous.aliases, external_id: externalId } : { external_id: externalId },
|
|
613
|
+
tags: anonymous ? { ...previous.tags } : {},
|
|
614
|
+
language: previous.language,
|
|
615
|
+
languageExplicit: previous.languageExplicit,
|
|
616
|
+
timezone: previous.timezone,
|
|
617
|
+
};
|
|
618
|
+
this.mutateUser(() => {
|
|
619
|
+
this.state.user = next;
|
|
620
|
+
});
|
|
621
|
+
this.queue.enqueue({
|
|
622
|
+
kind: 'login',
|
|
623
|
+
userLocalId: next.localId,
|
|
624
|
+
externalId,
|
|
625
|
+
previousLocalId: previous.localId,
|
|
626
|
+
previousWasAnonymous: anonymous,
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
logout() {
|
|
631
|
+
void this.command(() => {
|
|
632
|
+
const previous = this.state.user;
|
|
633
|
+
if (!previous.externalId) {
|
|
634
|
+
this.logger.debug('logout: no user is logged in');
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const next = {
|
|
638
|
+
...(0, state_1.createUser)(this.uuid()),
|
|
639
|
+
language: previous.language,
|
|
640
|
+
languageExplicit: previous.languageExplicit,
|
|
641
|
+
timezone: previous.timezone,
|
|
642
|
+
};
|
|
643
|
+
this.mutateUser(() => {
|
|
644
|
+
this.state.user = next;
|
|
645
|
+
});
|
|
646
|
+
this.queue.enqueue({
|
|
647
|
+
kind: 'createUser',
|
|
648
|
+
userLocalId: next.localId,
|
|
649
|
+
identity: {},
|
|
650
|
+
transferSubscription: true,
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
addTags(tags) {
|
|
655
|
+
void this.command(() => {
|
|
656
|
+
if (!tags || typeof tags !== 'object') {
|
|
657
|
+
this.logger.error('addTags: expected an object');
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const [normalized, rejected] = (0, tags_1.normalizeTags)(tags);
|
|
661
|
+
if (rejected.length > 0) {
|
|
662
|
+
this.logger.warn(`addTags: ignored invalid tags: ${rejected.join(', ')} (values must be strings or numbers)`);
|
|
663
|
+
}
|
|
664
|
+
const changes = (0, tags_1.changesForAdd)(this.state.user.tags, normalized);
|
|
665
|
+
this.applyLocalTagChanges(changes);
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
removeTags(keys) {
|
|
669
|
+
void this.command(() => {
|
|
670
|
+
if (!Array.isArray(keys)) {
|
|
671
|
+
this.logger.error('removeTags: expected an array of keys');
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
this.applyLocalTagChanges((0, tags_1.changesForRemove)(keys));
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
applyLocalTagChanges(changes) {
|
|
678
|
+
if ((0, tags_1.isEmpty)(changes))
|
|
679
|
+
return;
|
|
680
|
+
this.state.user.tags = (0, tags_1.applyTagChanges)(this.state.user.tags, changes);
|
|
681
|
+
this.save();
|
|
682
|
+
this.queue.enqueue({ kind: 'updateUser', userLocalId: this.state.user.localId, tags: changes });
|
|
683
|
+
}
|
|
684
|
+
getTags() {
|
|
685
|
+
return this.read(() => ({ ...this.state.user.tags }));
|
|
686
|
+
}
|
|
687
|
+
addAliases(aliases) {
|
|
688
|
+
void this.command(() => {
|
|
689
|
+
if (!aliases || typeof aliases !== 'object') {
|
|
690
|
+
this.logger.error('addAliases: expected an object');
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const valid = {};
|
|
694
|
+
for (const [label, id] of Object.entries(aliases)) {
|
|
695
|
+
if (typeof label !== 'string' || label.trim() === '' || RESERVED_ALIASES.includes(label)) {
|
|
696
|
+
this.logger.error(`addAlias: "${label}" cannot be used as an alias label (use login() for external_id)`);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
if (typeof id !== 'string' || id === '') {
|
|
700
|
+
this.logger.error(`addAlias: the id for "${label}" must be a non-empty string`);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
valid[label] = id;
|
|
704
|
+
}
|
|
705
|
+
if ((0, tags_1.isEmpty)(valid))
|
|
706
|
+
return;
|
|
707
|
+
this.state.user.aliases = { ...this.state.user.aliases, ...valid };
|
|
708
|
+
this.save();
|
|
709
|
+
this.queue.enqueue({ kind: 'setAlias', userLocalId: this.state.user.localId, aliases: valid });
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
removeAliases(labels) {
|
|
713
|
+
void this.command(() => {
|
|
714
|
+
if (!Array.isArray(labels))
|
|
715
|
+
return;
|
|
716
|
+
for (const label of labels) {
|
|
717
|
+
if (typeof label !== 'string' || label === '' || RESERVED_ALIASES.includes(label)) {
|
|
718
|
+
this.logger.error(`removeAlias: "${String(label)}" cannot be removed`);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const aliases = { ...this.state.user.aliases };
|
|
722
|
+
delete aliases[label];
|
|
723
|
+
this.state.user.aliases = aliases;
|
|
724
|
+
this.queue.enqueue({ kind: 'removeAlias', userLocalId: this.state.user.localId, label });
|
|
725
|
+
}
|
|
726
|
+
this.save();
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
setLanguage(language) {
|
|
730
|
+
void this.command(() => {
|
|
731
|
+
if (typeof language !== 'string' || language.trim() === '') {
|
|
732
|
+
this.logger.error('setLanguage: language must be a non-empty string');
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
this.state.user.language = language;
|
|
736
|
+
this.state.user.languageExplicit = true;
|
|
737
|
+
this.save();
|
|
738
|
+
this.queue.enqueue({
|
|
739
|
+
kind: 'updateUser',
|
|
740
|
+
userLocalId: this.state.user.localId,
|
|
741
|
+
properties: { language },
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
getQPushId() {
|
|
746
|
+
return this.read(() => { var _a; return (_a = this.state.user.qpushId) !== null && _a !== void 0 ? _a : null; });
|
|
747
|
+
}
|
|
748
|
+
getExternalId() {
|
|
749
|
+
return this.read(() => { var _a; return (_a = this.state.user.externalId) !== null && _a !== void 0 ? _a : null; });
|
|
750
|
+
}
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
// Sessions (AppState)
|
|
753
|
+
// ---------------------------------------------------------------------------
|
|
754
|
+
attachAppState() {
|
|
755
|
+
if (this.appStateSubscription)
|
|
756
|
+
return;
|
|
757
|
+
try {
|
|
758
|
+
this.appStateSubscription = this.platform.appState.addEventListener('change', (next) => this.onAppStateChange(next));
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
this.logger.warn('Could not observe AppState', error);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
startSession() {
|
|
765
|
+
var _a, _b;
|
|
766
|
+
const now = this.platform.now();
|
|
767
|
+
this.foregroundAt = now;
|
|
768
|
+
this.backgroundAt = null;
|
|
769
|
+
const user = this.state.user;
|
|
770
|
+
const properties = {};
|
|
771
|
+
const timezone = (_a = this.device) === null || _a === void 0 ? void 0 : _a.timezone;
|
|
772
|
+
if (timezone && timezone !== user.timezone) {
|
|
773
|
+
properties.timezone_id = timezone;
|
|
774
|
+
user.timezone = timezone;
|
|
775
|
+
}
|
|
776
|
+
const language = (_b = this.device) === null || _b === void 0 ? void 0 : _b.language;
|
|
777
|
+
if (!user.languageExplicit && language && language !== user.language) {
|
|
778
|
+
properties.language = language;
|
|
779
|
+
user.language = language;
|
|
780
|
+
}
|
|
781
|
+
const operation = {
|
|
782
|
+
kind: 'updateUser',
|
|
783
|
+
userLocalId: user.localId,
|
|
784
|
+
deltas: { session_count: 1 },
|
|
785
|
+
};
|
|
786
|
+
if (!(0, tags_1.isEmpty)(properties))
|
|
787
|
+
operation.properties = properties;
|
|
788
|
+
this.save();
|
|
789
|
+
this.queue.enqueue(operation);
|
|
790
|
+
this.logger.debug('New session');
|
|
791
|
+
void this.clearBadge(true);
|
|
792
|
+
}
|
|
793
|
+
onAppStateChange(next) {
|
|
794
|
+
if (next === 'active') {
|
|
795
|
+
void this.command(() => {
|
|
796
|
+
const now = this.platform.now();
|
|
797
|
+
if (this.pendingColdSession) {
|
|
798
|
+
this.pendingColdSession = false;
|
|
799
|
+
this.startSession();
|
|
800
|
+
}
|
|
801
|
+
else if (this.backgroundAt !== null && now - this.backgroundAt >= constants_1.SESSION_TIMEOUT_MS) {
|
|
802
|
+
this.startSession();
|
|
803
|
+
}
|
|
804
|
+
else if (this.backgroundAt !== null) {
|
|
805
|
+
this.foregroundAt = now;
|
|
806
|
+
this.backgroundAt = null;
|
|
807
|
+
void this.clearBadge(false);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
void this.refreshPermission(true);
|
|
811
|
+
this.retryRegistrationIfNeeded();
|
|
812
|
+
this.queue.kick();
|
|
813
|
+
}
|
|
814
|
+
else if (next === 'background') {
|
|
815
|
+
void this.command(() => {
|
|
816
|
+
const now = this.platform.now();
|
|
817
|
+
if (this.foregroundAt !== null) {
|
|
818
|
+
const seconds = Math.round((now - this.foregroundAt) / 1000);
|
|
819
|
+
this.foregroundAt = null;
|
|
820
|
+
if (seconds > 0) {
|
|
821
|
+
this.queue.enqueue({
|
|
822
|
+
kind: 'updateUser',
|
|
823
|
+
userLocalId: this.state.user.localId,
|
|
824
|
+
deltas: { session_time: seconds },
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (this.backgroundAt === null)
|
|
829
|
+
this.backgroundAt = now;
|
|
830
|
+
}).then(() => this.flushInBackground());
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async flushInBackground() {
|
|
834
|
+
if (!this.canProcess() || this.queue.size === 0)
|
|
835
|
+
return;
|
|
836
|
+
let taskId = -1;
|
|
837
|
+
try {
|
|
838
|
+
taskId = await this.platform.native.beginBackgroundTask();
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
taskId = -1;
|
|
842
|
+
}
|
|
843
|
+
let timer;
|
|
844
|
+
try {
|
|
845
|
+
await Promise.race([
|
|
846
|
+
this.queue.process().then(() => this.queue.idle()),
|
|
847
|
+
new Promise((resolve) => {
|
|
848
|
+
timer = this.platform.scheduler.setTimeout(resolve, constants_1.BACKGROUND_FLUSH_TIMEOUT_MS);
|
|
849
|
+
}),
|
|
850
|
+
]);
|
|
851
|
+
}
|
|
852
|
+
finally {
|
|
853
|
+
this.platform.scheduler.clearTimeout(timer);
|
|
854
|
+
if (taskId >= 0) {
|
|
855
|
+
try {
|
|
856
|
+
this.platform.native.endBackgroundTask(taskId);
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
// ignore
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async clearBadge(newSession) {
|
|
865
|
+
if (this.platform.os !== 'ios')
|
|
866
|
+
return;
|
|
867
|
+
let previous = 0;
|
|
868
|
+
try {
|
|
869
|
+
previous = await this.platform.native.clearBadge();
|
|
870
|
+
}
|
|
871
|
+
catch {
|
|
872
|
+
previous = 0;
|
|
873
|
+
}
|
|
874
|
+
if (previous > 0 || newSession) {
|
|
875
|
+
await this.command(() => {
|
|
876
|
+
this.mutateSubscription(() => {
|
|
877
|
+
this.state.subscription.badgeResetPending = true;
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// ---------------------------------------------------------------------------
|
|
883
|
+
// Notifications (native events)
|
|
884
|
+
// ---------------------------------------------------------------------------
|
|
885
|
+
/** Subscribes to native events and tells native that JS is ready. Idempotent. */
|
|
886
|
+
attachNativeEvents() {
|
|
887
|
+
if (this.nativeSubscriptions.length > 0)
|
|
888
|
+
return;
|
|
889
|
+
const source = this.platform.nativeEvents;
|
|
890
|
+
try {
|
|
891
|
+
this.nativeSubscriptions = [
|
|
892
|
+
source.addListener(constants_1.NativeEventNames.click, (payload) => this.handleNativeClick(payload)),
|
|
893
|
+
source.addListener(constants_1.NativeEventNames.foregroundWillDisplay, (payload) => this.handleNativeForeground(payload)),
|
|
894
|
+
source.addListener(constants_1.NativeEventNames.pushToken, (payload) => {
|
|
895
|
+
if (payload && typeof payload.token === 'string')
|
|
896
|
+
this.applyToken(payload.token);
|
|
897
|
+
}),
|
|
898
|
+
];
|
|
899
|
+
this.platform.native.setJsReady();
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
this.logger.error('Could not subscribe to native events', error);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
handleNativeClick(payload) {
|
|
906
|
+
var _a;
|
|
907
|
+
const data = (0, payload_1.parseNotificationPayload)(payload === null || payload === void 0 ? void 0 : payload.rawPayload);
|
|
908
|
+
if (!data) {
|
|
909
|
+
this.logger.debug('Ignoring click on a non-QPush notification');
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const event = {
|
|
913
|
+
result: {},
|
|
914
|
+
notification: new notification_1.QPushNotification(data),
|
|
915
|
+
};
|
|
916
|
+
if (payload.actionId)
|
|
917
|
+
event.result.actionId = payload.actionId;
|
|
918
|
+
const url = (_a = payload.url) !== null && _a !== void 0 ? _a : undefined;
|
|
919
|
+
if (url)
|
|
920
|
+
event.result.url = url;
|
|
921
|
+
if (this.events.count('click') > 0) {
|
|
922
|
+
this.emitClick(event);
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
this.pendingClicks.push(event);
|
|
926
|
+
if (this.pendingClicks.length > constants_1.MAX_PENDING_CLICKS)
|
|
927
|
+
this.pendingClicks.shift();
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
emitClick(event) {
|
|
931
|
+
this.events.emit('click', event, (error) => this.logger.error('click listener threw', error));
|
|
932
|
+
}
|
|
933
|
+
handleNativeForeground(payload) {
|
|
934
|
+
if (!payload || typeof payload.displayId !== 'string')
|
|
935
|
+
return;
|
|
936
|
+
const displayId = payload.displayId;
|
|
937
|
+
const data = (0, payload_1.parseNotificationPayload)(payload.rawPayload);
|
|
938
|
+
if (!data) {
|
|
939
|
+
this.platform.native.displayForegroundNotification(displayId);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const event = new notification_1.NotificationWillDisplayEvent(data, () => this.platform.native.displayForegroundNotification(displayId));
|
|
943
|
+
this.events.emit('foregroundWillDisplay', event, (error) => this.logger.error('foregroundWillDisplay listener threw', error));
|
|
944
|
+
if (event.settle() === 'prevented') {
|
|
945
|
+
this.platform.native.preventForegroundDisplay(displayId);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
addListener(event, listener) {
|
|
949
|
+
this.attachNativeEvents();
|
|
950
|
+
this.events.add(event, listener);
|
|
951
|
+
if (event === 'click' && this.pendingClicks.length > 0 && !this.clickFlushScheduled) {
|
|
952
|
+
// Deliver after the caller finished registering (cold start clicks).
|
|
953
|
+
this.clickFlushScheduled = true;
|
|
954
|
+
this.platform.scheduler.setTimeout(() => {
|
|
955
|
+
this.clickFlushScheduled = false;
|
|
956
|
+
const pending = this.pendingClicks;
|
|
957
|
+
this.pendingClicks = [];
|
|
958
|
+
for (const click of pending)
|
|
959
|
+
this.emitClick(click);
|
|
960
|
+
}, 0);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
removeListener(event, listener) {
|
|
964
|
+
this.events.remove(event, listener);
|
|
965
|
+
}
|
|
966
|
+
clearAll() {
|
|
967
|
+
try {
|
|
968
|
+
this.platform.native.clearAll();
|
|
969
|
+
}
|
|
970
|
+
catch (error) {
|
|
971
|
+
this.logger.error('clearAll failed', error);
|
|
972
|
+
}
|
|
973
|
+
void this.clearBadge(false);
|
|
974
|
+
}
|
|
975
|
+
removeNotification(notificationId) {
|
|
976
|
+
try {
|
|
977
|
+
this.platform.native.removeNotification(notificationId);
|
|
978
|
+
}
|
|
979
|
+
catch (error) {
|
|
980
|
+
this.logger.error('removeNotification failed', error);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
removeGroupedNotifications(groupKey) {
|
|
984
|
+
try {
|
|
985
|
+
this.platform.native.removeGroupedNotifications(groupKey);
|
|
986
|
+
}
|
|
987
|
+
catch (error) {
|
|
988
|
+
this.logger.error('removeGroupedNotifications failed', error);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
setAutoOpenUrls(enabled) {
|
|
992
|
+
try {
|
|
993
|
+
this.platform.native.setAutoOpenUrls(!!enabled);
|
|
994
|
+
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
this.logger.error('setAutoOpenUrls failed', error);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// ---------------------------------------------------------------------------
|
|
1000
|
+
// Operation execution
|
|
1001
|
+
// ---------------------------------------------------------------------------
|
|
1002
|
+
async execute(operation, item) {
|
|
1003
|
+
if (!this.state.config)
|
|
1004
|
+
return { type: 'retry', countAttempt: false };
|
|
1005
|
+
switch (operation.kind) {
|
|
1006
|
+
case 'createUser':
|
|
1007
|
+
return this.execCreateUser(operation);
|
|
1008
|
+
case 'login':
|
|
1009
|
+
return this.execLogin(operation);
|
|
1010
|
+
case 'updateUser':
|
|
1011
|
+
return this.execUpdateUser(operation);
|
|
1012
|
+
case 'setAlias':
|
|
1013
|
+
return this.execSetAlias(operation);
|
|
1014
|
+
case 'removeAlias':
|
|
1015
|
+
return this.execRemoveAlias(operation);
|
|
1016
|
+
case 'fetchUser':
|
|
1017
|
+
return this.execFetchUser(operation);
|
|
1018
|
+
case 'syncSubscription':
|
|
1019
|
+
return this.execSyncSubscription(item);
|
|
1020
|
+
case 'createSubscription':
|
|
1021
|
+
return this.execCreateSubscription(operation);
|
|
1022
|
+
default:
|
|
1023
|
+
return { type: 'drop', reason: 'unknown operation' };
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
userPath(qpushId) {
|
|
1027
|
+
return `/apps/${this.appId}/users/by/qpush_id/${(0, url_1.encodeSegment)(qpushId)}`;
|
|
1028
|
+
}
|
|
1029
|
+
userProperties(localId) {
|
|
1030
|
+
var _a, _b, _c, _d;
|
|
1031
|
+
const user = this.state.user.localId === localId ? this.state.user : undefined;
|
|
1032
|
+
const properties = {};
|
|
1033
|
+
const language = (_a = user === null || user === void 0 ? void 0 : user.language) !== null && _a !== void 0 ? _a : (_b = this.device) === null || _b === void 0 ? void 0 : _b.language;
|
|
1034
|
+
if (language)
|
|
1035
|
+
properties.language = language;
|
|
1036
|
+
if ((_c = this.device) === null || _c === void 0 ? void 0 : _c.timezone)
|
|
1037
|
+
properties.timezone_id = this.device.timezone;
|
|
1038
|
+
if ((_d = this.device) === null || _d === void 0 ? void 0 : _d.country)
|
|
1039
|
+
properties.country = this.device.country;
|
|
1040
|
+
return properties;
|
|
1041
|
+
}
|
|
1042
|
+
/** Tag changes still waiting in the queue for a user (to re-apply over server data). */
|
|
1043
|
+
pendingTagChanges(localId) {
|
|
1044
|
+
let changes = {};
|
|
1045
|
+
for (const item of this.queue.pending) {
|
|
1046
|
+
if (item.id === this.queue.inFlight)
|
|
1047
|
+
continue;
|
|
1048
|
+
const operation = item.operation;
|
|
1049
|
+
if (operation.kind === 'updateUser' && operation.userLocalId === localId && operation.tags) {
|
|
1050
|
+
changes = (0, tags_1.mergeTagChanges)(changes, operation.tags);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return changes;
|
|
1054
|
+
}
|
|
1055
|
+
pendingAliasChanges(localId, base) {
|
|
1056
|
+
const aliases = { ...base };
|
|
1057
|
+
for (const item of this.queue.pending) {
|
|
1058
|
+
if (item.id === this.queue.inFlight)
|
|
1059
|
+
continue;
|
|
1060
|
+
const operation = item.operation;
|
|
1061
|
+
if (operation.kind === 'setAlias' && operation.userLocalId === localId)
|
|
1062
|
+
Object.assign(aliases, operation.aliases);
|
|
1063
|
+
if (operation.kind === 'removeAlias' && operation.userLocalId === localId)
|
|
1064
|
+
delete aliases[operation.label];
|
|
1065
|
+
}
|
|
1066
|
+
return aliases;
|
|
1067
|
+
}
|
|
1068
|
+
mapUser(localId, qpushId, identity) {
|
|
1069
|
+
this.state.idMap[localId] = qpushId;
|
|
1070
|
+
if (this.state.user.localId === localId) {
|
|
1071
|
+
this.mutateUser(() => {
|
|
1072
|
+
const user = this.state.user;
|
|
1073
|
+
user.qpushId = qpushId;
|
|
1074
|
+
if (identity) {
|
|
1075
|
+
user.aliases = this.pendingAliasChanges(localId, aliasesFromIdentity(identity));
|
|
1076
|
+
const externalId = identity.external_id;
|
|
1077
|
+
if (typeof externalId === 'string' && externalId)
|
|
1078
|
+
user.externalId = externalId;
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
this.pruneIdMap();
|
|
1083
|
+
this.save();
|
|
1084
|
+
}
|
|
1085
|
+
/** Keeps only ids still referenced by the current user or pending operations. */
|
|
1086
|
+
pruneIdMap() {
|
|
1087
|
+
const referenced = new Set([this.state.user.localId]);
|
|
1088
|
+
for (const item of this.queue.pending) {
|
|
1089
|
+
const operation = item.operation;
|
|
1090
|
+
if ('userLocalId' in operation)
|
|
1091
|
+
referenced.add(operation.userLocalId);
|
|
1092
|
+
if (operation.kind === 'login')
|
|
1093
|
+
referenced.add(operation.previousLocalId);
|
|
1094
|
+
}
|
|
1095
|
+
for (const key of Object.keys(this.state.idMap)) {
|
|
1096
|
+
if (!referenced.has(key))
|
|
1097
|
+
delete this.state.idMap[key];
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
/** Picks this device's subscription in a user response. */
|
|
1101
|
+
findOwnSubscription(subscriptions, sent) {
|
|
1102
|
+
if (!Array.isArray(subscriptions) || subscriptions.length === 0)
|
|
1103
|
+
return undefined;
|
|
1104
|
+
const type = (0, subscription_1.subscriptionType)(this.platform.os);
|
|
1105
|
+
if (sent.id) {
|
|
1106
|
+
const byId = subscriptions.find((sub) => sub.id === sent.id);
|
|
1107
|
+
if (byId)
|
|
1108
|
+
return byId;
|
|
1109
|
+
}
|
|
1110
|
+
if (sent.token) {
|
|
1111
|
+
const byToken = subscriptions.find((sub) => sub.token === sent.token);
|
|
1112
|
+
if (byToken)
|
|
1113
|
+
return byToken;
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
const tokenless = subscriptions.filter((sub) => sub.type === type && !sub.token);
|
|
1117
|
+
if (tokenless.length === 1)
|
|
1118
|
+
return tokenless[0];
|
|
1119
|
+
}
|
|
1120
|
+
return subscriptions.length === 1 ? subscriptions[0] : undefined;
|
|
1121
|
+
}
|
|
1122
|
+
/** Applies a `POST /users` response (createUser, login fallback). */
|
|
1123
|
+
applyCreatedUser(localId, data, sentFields, sentId, repushLocalTags) {
|
|
1124
|
+
var _a, _b, _c;
|
|
1125
|
+
const identity = ((_a = data === null || data === void 0 ? void 0 : data.identity) !== null && _a !== void 0 ? _a : {});
|
|
1126
|
+
const qpushId = identityQPushId(identity);
|
|
1127
|
+
if (!qpushId)
|
|
1128
|
+
return { type: 'drop', reason: 'response without qpush_id' };
|
|
1129
|
+
this.mapUser(localId, qpushId, identity);
|
|
1130
|
+
if (this.state.user.localId === localId) {
|
|
1131
|
+
const serverTags = stringTags((_b = data === null || data === void 0 ? void 0 : data.properties) === null || _b === void 0 ? void 0 : _b.tags);
|
|
1132
|
+
const pending = this.pendingTagChanges(localId);
|
|
1133
|
+
const local = this.state.user.tags;
|
|
1134
|
+
// Re-push local tags the server does not have (never removals: a fresh
|
|
1135
|
+
// install must not wipe the tags of a user it was re-attached to).
|
|
1136
|
+
const missing = {};
|
|
1137
|
+
if (repushLocalTags) {
|
|
1138
|
+
for (const [key, value] of Object.entries(local)) {
|
|
1139
|
+
if (serverTags[key] !== value && !(key in pending))
|
|
1140
|
+
missing[key] = value;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
this.state.user.tags = (0, tags_1.applyTagChanges)((0, tags_1.applyTagChanges)(serverTags, missing), pending);
|
|
1144
|
+
if ((_c = this.device) === null || _c === void 0 ? void 0 : _c.timezone)
|
|
1145
|
+
this.state.user.timezone = this.device.timezone;
|
|
1146
|
+
if (!(0, tags_1.isEmpty)(missing)) {
|
|
1147
|
+
this.queue.enqueue({ kind: 'updateUser', userLocalId: localId, tags: missing });
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const own = this.findOwnSubscription(data === null || data === void 0 ? void 0 : data.subscriptions, { id: sentId, token: sentFields.token });
|
|
1151
|
+
if (own === null || own === void 0 ? void 0 : own.id) {
|
|
1152
|
+
this.state.subscription.synced = { ...sentFields };
|
|
1153
|
+
this.setSubscriptionId(own.id);
|
|
1154
|
+
}
|
|
1155
|
+
else {
|
|
1156
|
+
this.logger.warn('Could not identify this device subscription in the server response');
|
|
1157
|
+
}
|
|
1158
|
+
this.save();
|
|
1159
|
+
this.scheduleSyncIfDirty();
|
|
1160
|
+
return { type: 'success' };
|
|
1161
|
+
}
|
|
1162
|
+
async postUser(localId, identity, transferSubscription, repushLocalTags) {
|
|
1163
|
+
const fields = this.currentFields();
|
|
1164
|
+
const sentId = transferSubscription ? this.state.subscription.id : undefined;
|
|
1165
|
+
const body = {
|
|
1166
|
+
identity,
|
|
1167
|
+
properties: this.userProperties(localId),
|
|
1168
|
+
subscriptions: [(0, subscription_1.toSubscriptionPayload)(this.platform.os, fields, sentId)],
|
|
1169
|
+
};
|
|
1170
|
+
const response = await this.http.request('POST', `/apps/${this.appId}/users`, body);
|
|
1171
|
+
if (!response.ok)
|
|
1172
|
+
return (0, http_1.classifyResponse)(response);
|
|
1173
|
+
return this.applyCreatedUser(localId, response.data, fields, sentId, repushLocalTags);
|
|
1174
|
+
}
|
|
1175
|
+
execCreateUser(operation) {
|
|
1176
|
+
return this.postUser(operation.userLocalId, operation.identity, operation.transferSubscription, true);
|
|
1177
|
+
}
|
|
1178
|
+
async execLogin(operation) {
|
|
1179
|
+
var _a, _b, _c, _d, _e;
|
|
1180
|
+
const identity = { external_id: operation.externalId };
|
|
1181
|
+
// 1. Identify the anonymous user (keeps its tags), like OneSignal.
|
|
1182
|
+
const previousQPushId = this.state.idMap[operation.previousLocalId];
|
|
1183
|
+
// Anonymous data carries over only when the anonymous user is identified
|
|
1184
|
+
// (or never reached the server); it is discarded when switching users.
|
|
1185
|
+
let carryAnonymousData = operation.previousWasAnonymous;
|
|
1186
|
+
if (operation.previousWasAnonymous && previousQPushId) {
|
|
1187
|
+
const response = await this.http.request('PATCH', `${this.userPath(previousQPushId)}/identity`, { identity });
|
|
1188
|
+
if (response.ok) {
|
|
1189
|
+
this.mapUser(operation.userLocalId, previousQPushId, (_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.identity) !== null && _b !== void 0 ? _b : identity);
|
|
1190
|
+
return { type: 'success' };
|
|
1191
|
+
}
|
|
1192
|
+
if (response.status !== 409 && response.status !== 404)
|
|
1193
|
+
return (0, http_1.classifyResponse)(response);
|
|
1194
|
+
carryAnonymousData = false;
|
|
1195
|
+
this.logger.debug(`login: external id belongs to another user (${response.status}), switching user`);
|
|
1196
|
+
}
|
|
1197
|
+
// 2. Move this device's subscription to the user owning the external id.
|
|
1198
|
+
const subscriptionId = this.state.subscription.id;
|
|
1199
|
+
if (subscriptionId) {
|
|
1200
|
+
const response = await this.http.request('PATCH', `/apps/${this.appId}/subscriptions/${(0, url_1.encodeSegment)(subscriptionId)}/owner`, { identity });
|
|
1201
|
+
if (response.ok) {
|
|
1202
|
+
const qpushId = identityQPushId((_c = response.data) === null || _c === void 0 ? void 0 : _c.identity);
|
|
1203
|
+
if (!qpushId)
|
|
1204
|
+
return { type: 'drop', reason: 'owner response without qpush_id' };
|
|
1205
|
+
this.mapUser(operation.userLocalId, qpushId, (_e = (_d = response.data) === null || _d === void 0 ? void 0 : _d.identity) !== null && _e !== void 0 ? _e : identity);
|
|
1206
|
+
// The device switched to an existing user: refresh tags & aliases.
|
|
1207
|
+
this.queue.enqueue({ kind: 'fetchUser', userLocalId: operation.userLocalId });
|
|
1208
|
+
return { type: 'success' };
|
|
1209
|
+
}
|
|
1210
|
+
if (response.status !== 404)
|
|
1211
|
+
return (0, http_1.classifyResponse)(response);
|
|
1212
|
+
this.setSubscriptionId(undefined);
|
|
1213
|
+
this.state.subscription.synced = undefined;
|
|
1214
|
+
}
|
|
1215
|
+
// 3. No subscription on the server yet: create the user with it.
|
|
1216
|
+
return this.postUser(operation.userLocalId, identity, false, carryAnonymousData);
|
|
1217
|
+
}
|
|
1218
|
+
handleMissingUser(localId) {
|
|
1219
|
+
delete this.state.idMap[localId];
|
|
1220
|
+
if (this.state.user.localId !== localId)
|
|
1221
|
+
return;
|
|
1222
|
+
this.logger.warn('The current user no longer exists on the server, creating it again');
|
|
1223
|
+
const previous = this.state.user;
|
|
1224
|
+
const next = { ...previous, localId: this.uuid(), qpushId: undefined };
|
|
1225
|
+
this.mutateUser(() => {
|
|
1226
|
+
this.state.user = next;
|
|
1227
|
+
});
|
|
1228
|
+
this.queue.enqueue({
|
|
1229
|
+
kind: 'createUser',
|
|
1230
|
+
userLocalId: next.localId,
|
|
1231
|
+
identity: next.externalId ? { external_id: next.externalId } : {},
|
|
1232
|
+
transferSubscription: true,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
resolveQPushId(localId) {
|
|
1236
|
+
var _a;
|
|
1237
|
+
return (_a = this.state.idMap[localId]) !== null && _a !== void 0 ? _a : (this.state.user.localId === localId ? this.state.user.qpushId : undefined);
|
|
1238
|
+
}
|
|
1239
|
+
async execUpdateUser(operation) {
|
|
1240
|
+
var _a;
|
|
1241
|
+
const qpushId = this.resolveQPushId(operation.userLocalId);
|
|
1242
|
+
if (!qpushId)
|
|
1243
|
+
return { type: 'drop', reason: 'user was never created on the server' };
|
|
1244
|
+
const properties = { ...((_a = operation.properties) !== null && _a !== void 0 ? _a : {}) };
|
|
1245
|
+
if (!(0, tags_1.isEmpty)(operation.tags))
|
|
1246
|
+
properties.tags = operation.tags;
|
|
1247
|
+
const body = {};
|
|
1248
|
+
if (!(0, tags_1.isEmpty)(properties))
|
|
1249
|
+
body.properties = properties;
|
|
1250
|
+
if (!(0, tags_1.isEmpty)(operation.deltas))
|
|
1251
|
+
body.deltas = operation.deltas;
|
|
1252
|
+
if ((0, tags_1.isEmpty)(body))
|
|
1253
|
+
return { type: 'success' };
|
|
1254
|
+
const response = await this.http.request('PATCH', this.userPath(qpushId), body);
|
|
1255
|
+
if (response.status === 404) {
|
|
1256
|
+
this.handleMissingUser(operation.userLocalId);
|
|
1257
|
+
return { type: 'drop', reason: 'user not found' };
|
|
1258
|
+
}
|
|
1259
|
+
return (0, http_1.classifyResponse)(response);
|
|
1260
|
+
}
|
|
1261
|
+
async execSetAlias(operation) {
|
|
1262
|
+
var _a;
|
|
1263
|
+
const qpushId = this.resolveQPushId(operation.userLocalId);
|
|
1264
|
+
if (!qpushId)
|
|
1265
|
+
return { type: 'drop', reason: 'user was never created on the server' };
|
|
1266
|
+
const response = await this.http.request('PATCH', `${this.userPath(qpushId)}/identity`, { identity: operation.aliases });
|
|
1267
|
+
if (response.ok) {
|
|
1268
|
+
if (((_a = response.data) === null || _a === void 0 ? void 0 : _a.identity) && this.state.user.localId === operation.userLocalId) {
|
|
1269
|
+
this.mapUser(operation.userLocalId, qpushId, response.data.identity);
|
|
1270
|
+
}
|
|
1271
|
+
return { type: 'success' };
|
|
1272
|
+
}
|
|
1273
|
+
if (response.status === 409) {
|
|
1274
|
+
this.logger.error(`addAlias: alias already claimed by another user (${Object.keys(operation.aliases).join(', ')})`);
|
|
1275
|
+
if (this.state.user.localId === operation.userLocalId) {
|
|
1276
|
+
const aliases = { ...this.state.user.aliases };
|
|
1277
|
+
for (const [label, id] of Object.entries(operation.aliases))
|
|
1278
|
+
if (aliases[label] === id)
|
|
1279
|
+
delete aliases[label];
|
|
1280
|
+
this.state.user.aliases = aliases;
|
|
1281
|
+
this.save();
|
|
1282
|
+
}
|
|
1283
|
+
return { type: 'drop', reason: 'alias_claimed' };
|
|
1284
|
+
}
|
|
1285
|
+
if (response.status === 404) {
|
|
1286
|
+
this.handleMissingUser(operation.userLocalId);
|
|
1287
|
+
return { type: 'drop', reason: 'user not found' };
|
|
1288
|
+
}
|
|
1289
|
+
return (0, http_1.classifyResponse)(response);
|
|
1290
|
+
}
|
|
1291
|
+
async execRemoveAlias(operation) {
|
|
1292
|
+
const qpushId = this.resolveQPushId(operation.userLocalId);
|
|
1293
|
+
if (!qpushId)
|
|
1294
|
+
return { type: 'drop', reason: 'user was never created on the server' };
|
|
1295
|
+
const response = await this.http.request('DELETE', `${this.userPath(qpushId)}/identity/${(0, url_1.encodeSegment)(operation.label)}`);
|
|
1296
|
+
return (0, http_1.classifyResponse)(response);
|
|
1297
|
+
}
|
|
1298
|
+
async execFetchUser(operation) {
|
|
1299
|
+
var _a, _b, _c;
|
|
1300
|
+
const qpushId = this.resolveQPushId(operation.userLocalId);
|
|
1301
|
+
if (!qpushId)
|
|
1302
|
+
return { type: 'drop', reason: 'user was never created on the server' };
|
|
1303
|
+
const response = await this.http.request('GET', this.userPath(qpushId));
|
|
1304
|
+
if (response.status === 404) {
|
|
1305
|
+
this.handleMissingUser(operation.userLocalId);
|
|
1306
|
+
return { type: 'drop', reason: 'user not found' };
|
|
1307
|
+
}
|
|
1308
|
+
if (!response.ok)
|
|
1309
|
+
return (0, http_1.classifyResponse)(response);
|
|
1310
|
+
if (this.state.user.localId === operation.userLocalId && response.data) {
|
|
1311
|
+
const identity = ((_a = response.data.identity) !== null && _a !== void 0 ? _a : {});
|
|
1312
|
+
this.mapUser(operation.userLocalId, (_b = identityQPushId(identity)) !== null && _b !== void 0 ? _b : qpushId, identity);
|
|
1313
|
+
const serverTags = stringTags((_c = response.data.properties) === null || _c === void 0 ? void 0 : _c.tags);
|
|
1314
|
+
this.state.user.tags = (0, tags_1.applyTagChanges)(serverTags, this.pendingTagChanges(operation.userLocalId));
|
|
1315
|
+
this.save();
|
|
1316
|
+
}
|
|
1317
|
+
return { type: 'success' };
|
|
1318
|
+
}
|
|
1319
|
+
async execSyncSubscription(item) {
|
|
1320
|
+
var _a;
|
|
1321
|
+
const sub = this.state.subscription;
|
|
1322
|
+
if (!sub.id) {
|
|
1323
|
+
// A pending create carries the live snapshot; nothing to do now.
|
|
1324
|
+
if (this.queue.has((operation, other) => other.id !== item.id && (0, operations_1.createsSubscription)(operation))) {
|
|
1325
|
+
return { type: 'success' };
|
|
1326
|
+
}
|
|
1327
|
+
if (!this.state.user.qpushId)
|
|
1328
|
+
return { type: 'success' };
|
|
1329
|
+
return this.execCreateSubscription({ kind: 'createSubscription', userLocalId: this.state.user.localId });
|
|
1330
|
+
}
|
|
1331
|
+
const fields = this.currentFields();
|
|
1332
|
+
const patch = (0, subscription_1.diffSubscription)(sub.synced, fields);
|
|
1333
|
+
const resetBadge = !!sub.badgeResetPending;
|
|
1334
|
+
if (resetBadge)
|
|
1335
|
+
patch.badge_count = 0;
|
|
1336
|
+
if ((0, tags_1.isEmpty)(patch))
|
|
1337
|
+
return { type: 'success' };
|
|
1338
|
+
const response = await this.http.request('PATCH', `/apps/${this.appId}/subscriptions/${(0, url_1.encodeSegment)(sub.id)}`, { subscription: patch });
|
|
1339
|
+
if (response.ok) {
|
|
1340
|
+
const { badge_count: _ignored, ...sent } = patch;
|
|
1341
|
+
void _ignored;
|
|
1342
|
+
sub.synced = { ...((_a = sub.synced) !== null && _a !== void 0 ? _a : {}), ...sent };
|
|
1343
|
+
if (resetBadge)
|
|
1344
|
+
sub.badgeResetPending = false;
|
|
1345
|
+
this.save();
|
|
1346
|
+
this.scheduleSyncIfDirty();
|
|
1347
|
+
return { type: 'success' };
|
|
1348
|
+
}
|
|
1349
|
+
if (response.status === 404) {
|
|
1350
|
+
this.logger.warn('Subscription not found on the server, creating it again');
|
|
1351
|
+
sub.synced = undefined;
|
|
1352
|
+
this.setSubscriptionId(undefined);
|
|
1353
|
+
this.queue.enqueue({ kind: 'createSubscription', userLocalId: this.state.user.localId });
|
|
1354
|
+
return { type: 'drop', reason: 'subscription not found' };
|
|
1355
|
+
}
|
|
1356
|
+
return (0, http_1.classifyResponse)(response);
|
|
1357
|
+
}
|
|
1358
|
+
async execCreateSubscription(operation) {
|
|
1359
|
+
var _a, _b;
|
|
1360
|
+
if (this.state.subscription.id)
|
|
1361
|
+
return { type: 'success' };
|
|
1362
|
+
const qpushId = this.resolveQPushId(operation.userLocalId);
|
|
1363
|
+
if (!qpushId)
|
|
1364
|
+
return { type: 'drop', reason: 'user was never created on the server' };
|
|
1365
|
+
const fields = this.currentFields();
|
|
1366
|
+
const response = await this.http.request('POST', `${this.userPath(qpushId)}/subscriptions`, { subscription: (0, subscription_1.toSubscriptionPayload)(this.platform.os, fields) });
|
|
1367
|
+
if (response.ok) {
|
|
1368
|
+
const id = (_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.subscription) === null || _b === void 0 ? void 0 : _b.id;
|
|
1369
|
+
if (!id)
|
|
1370
|
+
return { type: 'drop', reason: 'response without subscription id' };
|
|
1371
|
+
this.state.subscription.synced = { ...fields };
|
|
1372
|
+
this.setSubscriptionId(id);
|
|
1373
|
+
this.scheduleSyncIfDirty();
|
|
1374
|
+
return { type: 'success' };
|
|
1375
|
+
}
|
|
1376
|
+
if (response.status === 404) {
|
|
1377
|
+
this.handleMissingUser(operation.userLocalId);
|
|
1378
|
+
return { type: 'drop', reason: 'user not found' };
|
|
1379
|
+
}
|
|
1380
|
+
return (0, http_1.classifyResponse)(response);
|
|
1381
|
+
}
|
|
1382
|
+
/** @internal Used by the facade to know whether `initialize` was called. */
|
|
1383
|
+
get isInitialized() {
|
|
1384
|
+
return this.initializeCalled;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
exports.QPushCore = QPushCore;
|