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.
Files changed (98) hide show
  1. package/README.md +439 -2
  2. package/android/build.gradle +77 -0
  3. package/android/gradle.properties +5 -0
  4. package/android/proguard-rules.pro +5 -0
  5. package/android/src/main/AndroidManifest.xml +29 -0
  6. package/android/src/main/java/com/qpush/reactnative/QPushDeviceInfo.kt +96 -0
  7. package/android/src/main/java/com/qpush/reactnative/QPushEventBus.kt +153 -0
  8. package/android/src/main/java/com/qpush/reactnative/QPushFcm.kt +108 -0
  9. package/android/src/main/java/com/qpush/reactnative/QPushHttp.kt +92 -0
  10. package/android/src/main/java/com/qpush/reactnative/QPushImages.kt +103 -0
  11. package/android/src/main/java/com/qpush/reactnative/QPushLog.kt +39 -0
  12. package/android/src/main/java/com/qpush/reactnative/QPushMessagingService.kt +77 -0
  13. package/android/src/main/java/com/qpush/reactnative/QPushNotificationOpenActivity.kt +121 -0
  14. package/android/src/main/java/com/qpush/reactnative/QPushNotificationRenderer.kt +256 -0
  15. package/android/src/main/java/com/qpush/reactnative/QPushPayload.kt +114 -0
  16. package/android/src/main/java/com/qpush/reactnative/QPushPushModule.kt +368 -0
  17. package/android/src/main/java/com/qpush/reactnative/QPushPushPackage.kt +15 -0
  18. package/android/src/main/java/com/qpush/reactnative/QPushStore.kt +108 -0
  19. package/android/src/main/res/values/qpush_strings.xml +6 -0
  20. package/app.plugin.js +1 -0
  21. package/ios/Extension/QPushNotificationServiceExtension.swift +261 -0
  22. package/ios/Extension/QPushPushExtension.podspec +34 -0
  23. package/ios/QPushPush.m +62 -0
  24. package/ios/QPushPush.swift +186 -0
  25. package/ios/QPushPushBridge.h +5 -0
  26. package/ios/QPushPushCore.swift +457 -0
  27. package/ios/QPushPushHooks.m +237 -0
  28. package/ios/QPushPushSupport.swift +249 -0
  29. package/ios_nse_template/Info.plist +38 -0
  30. package/ios_nse_template/NotificationService.entitlements +10 -0
  31. package/ios_nse_template/NotificationService.swift +44 -0
  32. package/lib/NativeQPushPush.d.ts +44 -0
  33. package/lib/NativeQPushPush.js +4 -0
  34. package/lib/QPush.d.ts +117 -0
  35. package/lib/QPush.js +206 -0
  36. package/lib/core/QPushCore.d.ts +148 -0
  37. package/lib/core/QPushCore.js +1387 -0
  38. package/lib/core/constants.d.ts +37 -0
  39. package/lib/core/constants.js +40 -0
  40. package/lib/core/events.d.ts +11 -0
  41. package/lib/core/events.js +40 -0
  42. package/lib/core/http.d.ts +41 -0
  43. package/lib/core/http.js +123 -0
  44. package/lib/core/logger.d.ts +18 -0
  45. package/lib/core/logger.js +43 -0
  46. package/lib/core/operationQueue.d.ts +117 -0
  47. package/lib/core/operationQueue.js +264 -0
  48. package/lib/core/operations.d.ts +80 -0
  49. package/lib/core/operations.js +88 -0
  50. package/lib/core/payload.d.ts +20 -0
  51. package/lib/core/payload.js +187 -0
  52. package/lib/core/platform.d.ts +72 -0
  53. package/lib/core/platform.js +7 -0
  54. package/lib/core/state.d.ts +51 -0
  55. package/lib/core/state.js +74 -0
  56. package/lib/core/storage.d.ts +34 -0
  57. package/lib/core/storage.js +68 -0
  58. package/lib/core/subscription.d.ts +67 -0
  59. package/lib/core/subscription.js +98 -0
  60. package/lib/core/tags.d.ts +39 -0
  61. package/lib/core/tags.js +123 -0
  62. package/lib/core/url.d.ts +17 -0
  63. package/lib/core/url.js +64 -0
  64. package/lib/core/uuid.d.ts +2 -0
  65. package/lib/core/uuid.js +18 -0
  66. package/lib/index.d.ts +7 -0
  67. package/lib/index.js +15 -0
  68. package/lib/notification.d.ts +67 -0
  69. package/lib/notification.js +73 -0
  70. package/lib/reactNativePlatform.d.ts +3 -0
  71. package/lib/reactNativePlatform.js +164 -0
  72. package/lib/types.d.ts +157 -0
  73. package/lib/types.js +31 -0
  74. package/package.json +70 -4
  75. package/plugin/withQPush.js +330 -0
  76. package/react-native-qpush.podspec +30 -0
  77. package/react-native.config.js +18 -0
  78. package/src/NativeQPushPush.ts +57 -0
  79. package/src/QPush.ts +292 -0
  80. package/src/core/QPushCore.ts +1515 -0
  81. package/src/core/constants.ts +42 -0
  82. package/src/core/events.ts +34 -0
  83. package/src/core/http.ts +146 -0
  84. package/src/core/logger.ts +42 -0
  85. package/src/core/operationQueue.ts +324 -0
  86. package/src/core/operations.ts +166 -0
  87. package/src/core/payload.ts +175 -0
  88. package/src/core/platform.ts +84 -0
  89. package/src/core/state.ts +127 -0
  90. package/src/core/storage.ts +76 -0
  91. package/src/core/subscription.ts +133 -0
  92. package/src/core/tags.ts +116 -0
  93. package/src/core/url.ts +58 -0
  94. package/src/core/uuid.ts +13 -0
  95. package/src/index.ts +23 -0
  96. package/src/notification.ts +113 -0
  97. package/src/reactNativePlatform.ts +172 -0
  98. package/src/types.ts +174 -0
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OperationQueue = void 0;
4
+ exports.computeBackoffDelay = computeBackoffDelay;
5
+ const logger_1 = require("./logger");
6
+ const operations_1 = require("./operations");
7
+ const platform_1 = require("./platform");
8
+ const storage_1 = require("./storage");
9
+ /**
10
+ * Exponential backoff: base * 2^(level-1), capped at max, ±25% jitter.
11
+ * A server provided `Retry-After` wins when it is larger.
12
+ */
13
+ function computeBackoffDelay(level, options) {
14
+ var _a;
15
+ const exponent = Math.max(0, level - 1);
16
+ const raw = Math.min(options.maxDelayMs, options.baseDelayMs * Math.pow(2, exponent));
17
+ const jitterFactor = 0.75 + Math.min(1, Math.max(0, options.random)) * 0.5;
18
+ let delay = Math.round(raw * jitterFactor);
19
+ if (options.retryAfterMs !== undefined && options.retryAfterMs > delay) {
20
+ delay = options.retryAfterMs;
21
+ }
22
+ return Math.min(Math.max(delay, 0), Math.max(options.maxDelayMs, (_a = options.retryAfterMs) !== null && _a !== void 0 ? _a : 0));
23
+ }
24
+ /**
25
+ * Persisted, ordered operation queue.
26
+ *
27
+ * - Operations run strictly one at a time, in order (head-of-line blocking is
28
+ * intended: later operations may depend on earlier ones).
29
+ * - Every change is persisted, so pending operations survive restarts.
30
+ * - Adjacent compatible operations are merged (see `mergeOperations`).
31
+ * - 429 / 5xx are retried with exponential backoff up to `maxAttempts`;
32
+ * network failures are retried without limit; other errors drop the item.
33
+ */
34
+ class OperationQueue {
35
+ constructor(options) {
36
+ var _a, _b, _c, _d, _e, _f, _g;
37
+ this.options = options;
38
+ this.items = [];
39
+ this.loaded = false;
40
+ this.inFlightId = null;
41
+ this.running = null;
42
+ this.retryTimer = null;
43
+ this.retryAt = null;
44
+ /** Consecutive failures of the head item (drives the backoff delay). */
45
+ this.backoffLevel = 0;
46
+ this.sequence = 0;
47
+ this.scheduler = (_a = options.scheduler) !== null && _a !== void 0 ? _a : platform_1.defaultScheduler;
48
+ this.now = (_b = options.now) !== null && _b !== void 0 ? _b : Date.now;
49
+ this.random = (_c = options.random) !== null && _c !== void 0 ? _c : Math.random;
50
+ this.logger = (_d = options.logger) !== null && _d !== void 0 ? _d : new logger_1.Logger();
51
+ this.maxAttempts = (_e = options.maxAttempts) !== null && _e !== void 0 ? _e : 5;
52
+ this.baseDelayMs = (_f = options.baseDelayMs) !== null && _f !== void 0 ? _f : 1000;
53
+ this.maxDelayMs = (_g = options.maxDelayMs) !== null && _g !== void 0 ? _g : 5 * 60000;
54
+ this.writer = new storage_1.SerialWriter(options.store, options.storageKey, (error) => this.logger.error('Failed to persist the operation queue', error));
55
+ }
56
+ /** Loads persisted items. Items enqueued before `load()` are kept after them. */
57
+ async load() {
58
+ if (this.loaded)
59
+ return;
60
+ const persisted = await (0, storage_1.readJson)(this.options.store, this.options.storageKey);
61
+ const restored = [];
62
+ if (Array.isArray(persisted)) {
63
+ for (const entry of persisted) {
64
+ if (entry &&
65
+ typeof entry === 'object' &&
66
+ typeof entry.id === 'string' &&
67
+ (0, operations_1.isValidOperation)(entry.operation)) {
68
+ const item = entry;
69
+ restored.push({
70
+ id: item.id,
71
+ operation: item.operation,
72
+ attempts: typeof item.attempts === 'number' ? item.attempts : 0,
73
+ createdAt: typeof item.createdAt === 'number' ? item.createdAt : this.now(),
74
+ });
75
+ }
76
+ }
77
+ }
78
+ const early = this.items;
79
+ this.items = restored;
80
+ this.loaded = true;
81
+ for (const item of early)
82
+ this.append(item.operation, false);
83
+ if (restored.length > 0)
84
+ this.logger.debug(`Restored ${restored.length} pending operation(s)`);
85
+ this.persist();
86
+ }
87
+ get isLoaded() {
88
+ return this.loaded;
89
+ }
90
+ /** Snapshot of pending items (head first). */
91
+ get pending() {
92
+ return this.items.slice();
93
+ }
94
+ get size() {
95
+ return this.items.length;
96
+ }
97
+ /** Id of the item currently executing, if any. */
98
+ get inFlight() {
99
+ return this.inFlightId;
100
+ }
101
+ /** Time of the next scheduled retry, if the queue is backing off. */
102
+ get nextRetryAt() {
103
+ return this.retryAt;
104
+ }
105
+ has(predicate) {
106
+ return this.items.some((item) => predicate(item.operation, item));
107
+ }
108
+ /** Adds an operation (merged into the tail when possible) and processes. */
109
+ enqueue(operation) {
110
+ this.append(operation, true);
111
+ this.process();
112
+ }
113
+ /** Removes pending (not in-flight) items matching the predicate. */
114
+ remove(predicate) {
115
+ const before = this.items.length;
116
+ this.items = this.items.filter((item) => item.id === this.inFlightId || !predicate(item.operation, item));
117
+ const removed = before - this.items.length;
118
+ if (removed > 0)
119
+ this.persist();
120
+ return removed;
121
+ }
122
+ /** Drops everything that is not in flight. */
123
+ clear() {
124
+ this.remove(() => true);
125
+ }
126
+ /** Cancels any backoff wait and processes immediately. */
127
+ kick() {
128
+ if (this.retryTimer !== null) {
129
+ this.scheduler.clearTimeout(this.retryTimer);
130
+ this.retryTimer = null;
131
+ this.retryAt = null;
132
+ }
133
+ this.process();
134
+ }
135
+ /** Resolves when the current processing run is over (empty, blocked or backing off). */
136
+ async idle() {
137
+ while (this.running)
138
+ await this.running;
139
+ }
140
+ /** Resolves when all pending writes reached the store. */
141
+ persisted() {
142
+ return this.writer.flush();
143
+ }
144
+ /** Starts a processing run if possible. Returns the run promise. */
145
+ process() {
146
+ if (this.running)
147
+ return this.running;
148
+ if (!this.loaded || this.retryTimer !== null || this.items.length === 0)
149
+ return Promise.resolve();
150
+ if (this.options.canProcess && !this.options.canProcess())
151
+ return Promise.resolve();
152
+ const run = this.runLoop().then(() => this.afterRun(run), (error) => {
153
+ this.logger.error('Operation queue run failed', error);
154
+ this.afterRun(run);
155
+ });
156
+ this.running = run;
157
+ return run;
158
+ }
159
+ afterRun(run) {
160
+ if (this.running === run)
161
+ this.running = null;
162
+ // Items enqueued while the loop was finishing must not be stranded.
163
+ if (this.items.length > 0 &&
164
+ this.retryTimer === null &&
165
+ (!this.options.canProcess || this.options.canProcess())) {
166
+ void this.process();
167
+ }
168
+ }
169
+ append(operation, persist) {
170
+ const tail = this.items[this.items.length - 1];
171
+ if (operation.kind === 'syncSubscription') {
172
+ // State based: one pending (not in-flight) sync is always enough.
173
+ const pendingSync = this.items.some((item) => item.operation.kind === 'syncSubscription' && item.id !== this.inFlightId);
174
+ if (pendingSync)
175
+ return;
176
+ }
177
+ if (tail && this.loaded && tail.id !== this.inFlightId) {
178
+ const merged = (0, operations_1.mergeOperations)(tail.operation, operation);
179
+ if (merged) {
180
+ tail.operation = merged;
181
+ if (persist)
182
+ this.persist();
183
+ return;
184
+ }
185
+ }
186
+ this.items.push({ id: this.nextId(), operation, attempts: 0, createdAt: this.now() });
187
+ if (persist && this.loaded)
188
+ this.persist();
189
+ }
190
+ nextId() {
191
+ this.sequence += 1;
192
+ return `${this.now().toString(36)}-${this.sequence.toString(36)}-${Math.floor(this.random() * 1e9).toString(36)}`;
193
+ }
194
+ persist() {
195
+ if (!this.loaded)
196
+ return;
197
+ void this.writer.write(JSON.stringify(this.items));
198
+ }
199
+ async runLoop() {
200
+ var _a, _b, _c, _d, _e;
201
+ while (this.items.length > 0) {
202
+ if (this.options.canProcess && !this.options.canProcess())
203
+ return;
204
+ const item = this.items[0];
205
+ this.inFlightId = item.id;
206
+ let result;
207
+ try {
208
+ result = await this.options.execute(item.operation, item);
209
+ }
210
+ catch (error) {
211
+ this.logger.error(`Operation ${item.operation.kind} threw, dropping it`, error);
212
+ result = { type: 'drop', reason: 'exception' };
213
+ }
214
+ finally {
215
+ this.inFlightId = null;
216
+ }
217
+ if (result.type === 'success' || result.type === 'drop') {
218
+ this.removeById(item.id);
219
+ this.backoffLevel = 0;
220
+ if (result.type === 'drop') {
221
+ const reason = (_a = result.reason) !== null && _a !== void 0 ? _a : 'not retryable';
222
+ this.logger.warn(`Dropped operation ${item.operation.kind}: ${reason}`);
223
+ (_c = (_b = this.options).onDrop) === null || _c === void 0 ? void 0 : _c.call(_b, item, reason);
224
+ }
225
+ this.persist();
226
+ continue;
227
+ }
228
+ // Retry.
229
+ if (result.countAttempt !== false)
230
+ item.attempts += 1;
231
+ if (item.attempts >= this.maxAttempts) {
232
+ this.removeById(item.id);
233
+ this.backoffLevel = 0;
234
+ const reason = `gave up after ${item.attempts} attempts`;
235
+ this.logger.warn(`Dropped operation ${item.operation.kind}: ${reason}`);
236
+ (_e = (_d = this.options).onDrop) === null || _e === void 0 ? void 0 : _e.call(_d, item, reason);
237
+ this.persist();
238
+ continue;
239
+ }
240
+ this.backoffLevel += 1;
241
+ this.persist();
242
+ const delay = computeBackoffDelay(this.backoffLevel, {
243
+ baseDelayMs: this.baseDelayMs,
244
+ maxDelayMs: this.maxDelayMs,
245
+ random: this.random(),
246
+ retryAfterMs: result.retryAfterMs,
247
+ });
248
+ this.logger.debug(`Retrying ${item.operation.kind} in ${delay}ms`);
249
+ this.retryAt = this.now() + delay;
250
+ this.retryTimer = this.scheduler.setTimeout(() => {
251
+ this.retryTimer = null;
252
+ this.retryAt = null;
253
+ void this.process();
254
+ }, delay);
255
+ return;
256
+ }
257
+ }
258
+ removeById(id) {
259
+ const index = this.items.findIndex((entry) => entry.id === id);
260
+ if (index >= 0)
261
+ this.items.splice(index, 1);
262
+ }
263
+ }
264
+ exports.OperationQueue = OperationQueue;
@@ -0,0 +1,80 @@
1
+ import { type TagChanges } from './tags';
2
+ /**
3
+ * Operations are persisted in the queue as plain JSON. Every user-scoped
4
+ * operation references the user by a *local id* generated on the device; the
5
+ * real `qpush_id` is resolved when the operation executes (the user may not
6
+ * exist on the server yet when the operation is enqueued). This keeps changes
7
+ * made before `login()`/`logout()` attached to the right user.
8
+ */
9
+ /** `POST /apps/{app_id}/users` (cold start for a new device, and `logout()`). */
10
+ export interface CreateUserOperation {
11
+ kind: 'createUser';
12
+ userLocalId: string;
13
+ identity: Record<string, string>;
14
+ /** Include the current subscription id so the server moves it (logout). */
15
+ transferSubscription: boolean;
16
+ }
17
+ /** `login(externalId)`: identify the anonymous user or switch users. */
18
+ export interface LoginOperation {
19
+ kind: 'login';
20
+ userLocalId: string;
21
+ externalId: string;
22
+ previousLocalId: string;
23
+ previousWasAnonymous: boolean;
24
+ }
25
+ /** `PATCH /apps/{app_id}/users/by/qpush_id/{id}` */
26
+ export interface UpdateUserOperation {
27
+ kind: 'updateUser';
28
+ userLocalId: string;
29
+ tags?: TagChanges;
30
+ properties?: {
31
+ language?: string;
32
+ timezone_id?: string;
33
+ country?: string;
34
+ };
35
+ deltas?: {
36
+ session_count?: number;
37
+ session_time?: number;
38
+ };
39
+ }
40
+ /** `PATCH .../identity` */
41
+ export interface SetAliasOperation {
42
+ kind: 'setAlias';
43
+ userLocalId: string;
44
+ aliases: Record<string, string>;
45
+ }
46
+ /** `DELETE .../identity/{label}` */
47
+ export interface RemoveAliasOperation {
48
+ kind: 'removeAlias';
49
+ userLocalId: string;
50
+ label: string;
51
+ }
52
+ /** `GET /apps/{app_id}/users/by/qpush_id/{id}` to refresh the local cache. */
53
+ export interface FetchUserOperation {
54
+ kind: 'fetchUser';
55
+ userLocalId: string;
56
+ }
57
+ /**
58
+ * State based: computes the diff between the local subscription and what the
59
+ * server acknowledged at execution time, then `PATCH /subscriptions/{id}`.
60
+ */
61
+ export interface SyncSubscriptionOperation {
62
+ kind: 'syncSubscription';
63
+ }
64
+ /** `POST .../users/by/qpush_id/{id}/subscriptions` (subscription lost server side). */
65
+ export interface CreateSubscriptionOperation {
66
+ kind: 'createSubscription';
67
+ userLocalId: string;
68
+ }
69
+ export type Operation = CreateUserOperation | LoginOperation | UpdateUserOperation | SetAliasOperation | RemoveAliasOperation | FetchUserOperation | SyncSubscriptionOperation | CreateSubscriptionOperation;
70
+ export type OperationKind = Operation['kind'];
71
+ /**
72
+ * Tries to merge `next` into `previous` (the tail of the queue). Returns the
73
+ * merged operation, or `null` when they cannot be merged.
74
+ */
75
+ export declare function mergeOperations(previous: Operation, next: Operation): Operation | null;
76
+ /** Operations that (re)create the device subscription on the server. */
77
+ export declare function createsSubscription(operation: Operation): boolean;
78
+ /** Returns the local user id an operation is scoped to, if any. */
79
+ export declare function operationUserLocalId(operation: Operation): string | undefined;
80
+ export declare function isValidOperation(value: unknown): value is Operation;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeOperations = mergeOperations;
4
+ exports.createsSubscription = createsSubscription;
5
+ exports.operationUserLocalId = operationUserLocalId;
6
+ exports.isValidOperation = isValidOperation;
7
+ const tags_1 = require("./tags");
8
+ function sumDelta(a, b) {
9
+ if (a === undefined)
10
+ return b;
11
+ if (b === undefined)
12
+ return a;
13
+ return a + b;
14
+ }
15
+ function stripEmpty(value) {
16
+ if (!value)
17
+ return undefined;
18
+ const result = {};
19
+ for (const key of Object.keys(value)) {
20
+ if (value[key] !== undefined)
21
+ result[key] = value[key];
22
+ }
23
+ return Object.keys(result).length > 0 ? result : undefined;
24
+ }
25
+ /**
26
+ * Tries to merge `next` into `previous` (the tail of the queue). Returns the
27
+ * merged operation, or `null` when they cannot be merged.
28
+ */
29
+ function mergeOperations(previous, next) {
30
+ var _a, _b, _c, _d, _e, _f, _g, _h;
31
+ if (previous.kind === 'updateUser' && next.kind === 'updateUser') {
32
+ if (previous.userLocalId !== next.userLocalId)
33
+ return null;
34
+ const tags = previous.tags || next.tags ? (0, tags_1.mergeTagChanges)((_a = previous.tags) !== null && _a !== void 0 ? _a : {}, (_b = next.tags) !== null && _b !== void 0 ? _b : {}) : undefined;
35
+ const properties = stripEmpty({ ...((_c = previous.properties) !== null && _c !== void 0 ? _c : {}), ...((_d = next.properties) !== null && _d !== void 0 ? _d : {}) });
36
+ const deltas = stripEmpty({
37
+ session_count: sumDelta((_e = previous.deltas) === null || _e === void 0 ? void 0 : _e.session_count, (_f = next.deltas) === null || _f === void 0 ? void 0 : _f.session_count),
38
+ session_time: sumDelta((_g = previous.deltas) === null || _g === void 0 ? void 0 : _g.session_time, (_h = next.deltas) === null || _h === void 0 ? void 0 : _h.session_time),
39
+ });
40
+ const merged = { kind: 'updateUser', userLocalId: next.userLocalId };
41
+ if (tags && Object.keys(tags).length > 0)
42
+ merged.tags = tags;
43
+ if (properties)
44
+ merged.properties = properties;
45
+ if (deltas)
46
+ merged.deltas = deltas;
47
+ return merged;
48
+ }
49
+ if (previous.kind === 'setAlias' && next.kind === 'setAlias') {
50
+ if (previous.userLocalId !== next.userLocalId)
51
+ return null;
52
+ return { kind: 'setAlias', userLocalId: next.userLocalId, aliases: { ...previous.aliases, ...next.aliases } };
53
+ }
54
+ if (previous.kind === 'syncSubscription' && next.kind === 'syncSubscription') {
55
+ return previous;
56
+ }
57
+ if (previous.kind === 'fetchUser' && next.kind === 'fetchUser' && previous.userLocalId === next.userLocalId) {
58
+ return previous;
59
+ }
60
+ return null;
61
+ }
62
+ /** Operations that (re)create the device subscription on the server. */
63
+ function createsSubscription(operation) {
64
+ return operation.kind === 'createUser' || operation.kind === 'login' || operation.kind === 'createSubscription';
65
+ }
66
+ /** Returns the local user id an operation is scoped to, if any. */
67
+ function operationUserLocalId(operation) {
68
+ return 'userLocalId' in operation ? operation.userLocalId : undefined;
69
+ }
70
+ function isValidOperation(value) {
71
+ if (typeof value !== 'object' || value === null)
72
+ return false;
73
+ const kind = value.kind;
74
+ switch (kind) {
75
+ case 'createUser':
76
+ case 'login':
77
+ case 'updateUser':
78
+ case 'setAlias':
79
+ case 'removeAlias':
80
+ case 'fetchUser':
81
+ case 'createSubscription':
82
+ return typeof value.userLocalId === 'string';
83
+ case 'syncSubscription':
84
+ return true;
85
+ default:
86
+ return false;
87
+ }
88
+ }
@@ -0,0 +1,20 @@
1
+ import type { ActionButton, QPushNotificationData } from '../types';
2
+ type JsonObject = Record<string, unknown>;
3
+ export declare function parseButtons(value: unknown): ActionButton[] | undefined;
4
+ /**
5
+ * Parses the Android `data["qpush"]` JSON (already decoded) into the
6
+ * normalized notification shape.
7
+ */
8
+ export declare function parseAndroidPayload(qpush: JsonObject, raw?: JsonObject): QPushNotificationData | null;
9
+ /** Parses an iOS `userInfo` dictionary (`aps` + `qpush`). */
10
+ export declare function parseIosPayload(userInfo: JsonObject): QPushNotificationData | null;
11
+ /**
12
+ * Parses whatever the native layer forwarded:
13
+ * - Android: the `data["qpush"]` JSON string, the decoded object, or the
14
+ * whole FCM data map (`{ qpush: "<json>" }`)
15
+ * - iOS: the notification `userInfo` (`{ aps, qpush }`)
16
+ *
17
+ * Returns `null` when the payload is not a QPush notification.
18
+ */
19
+ export declare function parseNotificationPayload(raw: unknown): QPushNotificationData | null;
20
+ export {};
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseButtons = parseButtons;
4
+ exports.parseAndroidPayload = parseAndroidPayload;
5
+ exports.parseIosPayload = parseIosPayload;
6
+ exports.parseNotificationPayload = parseNotificationPayload;
7
+ function isObject(value) {
8
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
9
+ }
10
+ function asString(value) {
11
+ if (typeof value === 'string')
12
+ return value.length > 0 ? value : undefined;
13
+ if (typeof value === 'number' && Number.isFinite(value))
14
+ return String(value);
15
+ return undefined;
16
+ }
17
+ function asNumber(value) {
18
+ if (typeof value === 'number' && Number.isFinite(value))
19
+ return value;
20
+ if (typeof value === 'string' && value.trim() !== '') {
21
+ const parsed = Number(value);
22
+ if (Number.isFinite(parsed))
23
+ return parsed;
24
+ }
25
+ return undefined;
26
+ }
27
+ function tryParseJson(value) {
28
+ try {
29
+ return JSON.parse(value);
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ /** `data` is normally an object but tolerate a JSON string. */
36
+ function asDataObject(value) {
37
+ if (isObject(value))
38
+ return value;
39
+ if (typeof value === 'string') {
40
+ const parsed = tryParseJson(value);
41
+ if (isObject(parsed))
42
+ return parsed;
43
+ }
44
+ return undefined;
45
+ }
46
+ function parseButtons(value) {
47
+ let list = value;
48
+ if (typeof list === 'string')
49
+ list = tryParseJson(list);
50
+ if (!Array.isArray(list))
51
+ return undefined;
52
+ const buttons = [];
53
+ for (const entry of list) {
54
+ if (!isObject(entry))
55
+ continue;
56
+ const id = asString(entry.id);
57
+ const text = asString(entry.text);
58
+ if (!id || !text)
59
+ continue;
60
+ const button = { id, text };
61
+ const icon = asString(entry.icon);
62
+ const url = asString(entry.url);
63
+ if (icon)
64
+ button.icon = icon;
65
+ if (url)
66
+ button.url = url;
67
+ buttons.push(button);
68
+ }
69
+ return buttons.length > 0 ? buttons : undefined;
70
+ }
71
+ function stripUndefined(value) {
72
+ const result = { ...value };
73
+ for (const key of Object.keys(result)) {
74
+ if (result[key] === undefined)
75
+ delete result[key];
76
+ }
77
+ return result;
78
+ }
79
+ /**
80
+ * Parses the Android `data["qpush"]` JSON (already decoded) into the
81
+ * normalized notification shape.
82
+ */
83
+ function parseAndroidPayload(qpush, raw = qpush) {
84
+ const notificationId = asString(qpush.id);
85
+ if (!notificationId)
86
+ return null;
87
+ return stripUndefined({
88
+ notificationId,
89
+ title: asString(qpush.title),
90
+ body: asString(qpush.body),
91
+ launchURL: asString(qpush.url),
92
+ additionalData: asDataObject(qpush.data),
93
+ bigPicture: asString(qpush.image),
94
+ largeIcon: asString(qpush.large_icon),
95
+ smallIcon: asString(qpush.small_icon),
96
+ accentColor: asString(qpush.accent_color),
97
+ sound: asString(qpush.sound),
98
+ channelId: asString(qpush.channel_id),
99
+ priority: asNumber(qpush.priority),
100
+ groupKey: asString(qpush.group),
101
+ collapseId: asString(qpush.collapse_id),
102
+ visibility: asNumber(qpush.visibility),
103
+ actionButtons: parseButtons(qpush.buttons),
104
+ rawPayload: raw,
105
+ });
106
+ }
107
+ /** Parses an iOS `userInfo` dictionary (`aps` + `qpush`). */
108
+ function parseIosPayload(userInfo) {
109
+ let qpush = userInfo.qpush;
110
+ if (typeof qpush === 'string')
111
+ qpush = tryParseJson(qpush);
112
+ if (!isObject(qpush))
113
+ return null;
114
+ const notificationId = asString(qpush.id);
115
+ if (!notificationId)
116
+ return null;
117
+ const aps = isObject(userInfo.aps) ? userInfo.aps : {};
118
+ let title;
119
+ let subtitle;
120
+ let body;
121
+ const alert = aps.alert;
122
+ if (typeof alert === 'string') {
123
+ body = asString(alert);
124
+ }
125
+ else if (isObject(alert)) {
126
+ title = asString(alert.title);
127
+ subtitle = asString(alert.subtitle);
128
+ body = asString(alert.body);
129
+ }
130
+ let sound;
131
+ if (typeof aps.sound === 'string')
132
+ sound = asString(aps.sound);
133
+ else if (isObject(aps.sound))
134
+ sound = asString(aps.sound.name);
135
+ const contentAvailable = asNumber(aps['content-available']);
136
+ return stripUndefined({
137
+ notificationId,
138
+ title,
139
+ subtitle,
140
+ body,
141
+ launchURL: asString(qpush.url),
142
+ additionalData: asDataObject(qpush.data),
143
+ bigPicture: asString(qpush.image),
144
+ sound,
145
+ badge: asNumber(aps.badge),
146
+ category: asString(aps.category),
147
+ groupKey: asString(aps['thread-id']),
148
+ interruptionLevel: asString(aps['interruption-level']),
149
+ relevanceScore: asNumber(aps['relevance-score']),
150
+ contentAvailable: contentAvailable === 1 ? true : undefined,
151
+ actionButtons: parseButtons(qpush.buttons),
152
+ rawPayload: userInfo,
153
+ });
154
+ }
155
+ /**
156
+ * Parses whatever the native layer forwarded:
157
+ * - Android: the `data["qpush"]` JSON string, the decoded object, or the
158
+ * whole FCM data map (`{ qpush: "<json>" }`)
159
+ * - iOS: the notification `userInfo` (`{ aps, qpush }`)
160
+ *
161
+ * Returns `null` when the payload is not a QPush notification.
162
+ */
163
+ function parseNotificationPayload(raw) {
164
+ let value = raw;
165
+ if (typeof value === 'string')
166
+ value = tryParseJson(value);
167
+ if (!isObject(value))
168
+ return null;
169
+ // iOS userInfo (always carries `aps`).
170
+ if (isObject(value.aps))
171
+ return parseIosPayload(value);
172
+ // A data map whose `qpush` entry was already decoded. The iOS `qpush`
173
+ // keys (id/url/data/image/buttons) are a subset of the Android ones.
174
+ if (isObject(value.qpush))
175
+ return parseAndroidPayload(value.qpush, value);
176
+ // Android FCM data map: { qpush: "<json string>" }
177
+ if (typeof value.qpush === 'string') {
178
+ const inner = tryParseJson(value.qpush);
179
+ if (!isObject(inner))
180
+ return null;
181
+ return parseAndroidPayload(inner, inner);
182
+ }
183
+ // Android `qpush` object itself.
184
+ if ('id' in value)
185
+ return parseAndroidPayload(value);
186
+ return null;
187
+ }