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,72 @@
|
|
|
1
|
+
import type { FcmParams, PermissionStatus } from '../types';
|
|
2
|
+
import type { KeyValueStore } from './storage';
|
|
3
|
+
import type { DeviceInfo } from './subscription';
|
|
4
|
+
export interface Scheduler {
|
|
5
|
+
setTimeout(callback: () => void, ms: number): unknown;
|
|
6
|
+
clearTimeout(handle: unknown): void;
|
|
7
|
+
}
|
|
8
|
+
export interface Subscription {
|
|
9
|
+
remove(): void;
|
|
10
|
+
}
|
|
11
|
+
export interface FetchResponseLike {
|
|
12
|
+
status: number;
|
|
13
|
+
headers: {
|
|
14
|
+
get(name: string): string | null;
|
|
15
|
+
};
|
|
16
|
+
text(): Promise<string>;
|
|
17
|
+
}
|
|
18
|
+
export interface FetchInit {
|
|
19
|
+
method: string;
|
|
20
|
+
headers: Record<string, string>;
|
|
21
|
+
body?: string;
|
|
22
|
+
signal?: unknown;
|
|
23
|
+
}
|
|
24
|
+
export type FetchLike = (url: string, init: FetchInit) => Promise<FetchResponseLike>;
|
|
25
|
+
export interface AppStateLike {
|
|
26
|
+
readonly currentState: string | null | undefined;
|
|
27
|
+
addEventListener(type: 'change', listener: (state: string) => void): Subscription;
|
|
28
|
+
}
|
|
29
|
+
export interface NativeEventSource {
|
|
30
|
+
addListener(eventName: string, listener: (payload: any) => void): Subscription;
|
|
31
|
+
}
|
|
32
|
+
/** Result of `registerForPush`. */
|
|
33
|
+
export interface NativePushRegistration {
|
|
34
|
+
token: string | null;
|
|
35
|
+
/** Negative `notification_types` value when registration failed. */
|
|
36
|
+
notificationTypes?: number;
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Typed facade over the native module (see `NativeQPushPush.ts`). */
|
|
40
|
+
export interface NativeBridge extends KeyValueStore {
|
|
41
|
+
getDeviceInfo(): Promise<DeviceInfo>;
|
|
42
|
+
registerForPush(fcmParams: FcmParams | null): Promise<NativePushRegistration>;
|
|
43
|
+
getPermissionStatus(): Promise<PermissionStatus>;
|
|
44
|
+
requestPermission(fallbackToSettings: boolean): Promise<boolean>;
|
|
45
|
+
registerForProvisionalAuthorization(): Promise<boolean>;
|
|
46
|
+
canRequestPermission(): Promise<boolean>;
|
|
47
|
+
clearAll(): void;
|
|
48
|
+
removeNotification(notificationId: string): void;
|
|
49
|
+
removeGroupedNotifications(groupKey: string): void;
|
|
50
|
+
/** Clears the app badge and returns the previous value (iOS; 0 on Android). */
|
|
51
|
+
clearBadge(): Promise<number>;
|
|
52
|
+
/** Tells native that JS subscribed to events: queued native events are flushed. */
|
|
53
|
+
setJsReady(): void;
|
|
54
|
+
displayForegroundNotification(displayId: string): void;
|
|
55
|
+
preventForegroundDisplay(displayId: string): void;
|
|
56
|
+
setAutoOpenUrls(enabled: boolean): void;
|
|
57
|
+
setLogLevel(level: number): void;
|
|
58
|
+
beginBackgroundTask(): Promise<number>;
|
|
59
|
+
endBackgroundTask(taskId: number): void;
|
|
60
|
+
}
|
|
61
|
+
/** Everything the core needs from the host environment (injected for tests). */
|
|
62
|
+
export interface PlatformAdapter {
|
|
63
|
+
os: 'ios' | 'android';
|
|
64
|
+
native: NativeBridge;
|
|
65
|
+
nativeEvents: NativeEventSource;
|
|
66
|
+
appState: AppStateLike;
|
|
67
|
+
fetch: FetchLike;
|
|
68
|
+
scheduler: Scheduler;
|
|
69
|
+
now(): number;
|
|
70
|
+
random(): number;
|
|
71
|
+
}
|
|
72
|
+
export declare const defaultScheduler: Scheduler;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { FcmParams, PermissionStatus } from '../types';
|
|
2
|
+
import type { SubscriptionFields } from './subscription';
|
|
3
|
+
/** Local model of the current user. */
|
|
4
|
+
export interface PersistedUser {
|
|
5
|
+
/** Device-generated id; operations reference users by it. */
|
|
6
|
+
localId: string;
|
|
7
|
+
qpushId?: string;
|
|
8
|
+
externalId?: string;
|
|
9
|
+
/** Aliases except `qpush_id` / `onesignal_id` (includes `external_id`). */
|
|
10
|
+
aliases: Record<string, string>;
|
|
11
|
+
tags: Record<string, string>;
|
|
12
|
+
language?: string;
|
|
13
|
+
/** `true` once the developer called `setLanguage()`. */
|
|
14
|
+
languageExplicit?: boolean;
|
|
15
|
+
/** Last timezone sent to the server. */
|
|
16
|
+
timezone?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Local model of this device's push subscription. */
|
|
19
|
+
export interface PersistedSubscription {
|
|
20
|
+
id?: string;
|
|
21
|
+
token?: string;
|
|
22
|
+
/** Negative notification_types from a failed native registration. */
|
|
23
|
+
tokenError?: number;
|
|
24
|
+
/** Developer intent: `optIn()` / `optOut()`. */
|
|
25
|
+
optedIn: boolean;
|
|
26
|
+
permission: PermissionStatus;
|
|
27
|
+
/** Fields last acknowledged by the server. */
|
|
28
|
+
synced?: Partial<SubscriptionFields>;
|
|
29
|
+
/** Send `badge_count: 0` with the next subscription PATCH. */
|
|
30
|
+
badgeResetPending?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface PersistedConfig {
|
|
33
|
+
appId: string;
|
|
34
|
+
serverUrl: string;
|
|
35
|
+
}
|
|
36
|
+
export interface PersistedState {
|
|
37
|
+
version: 1;
|
|
38
|
+
config?: PersistedConfig;
|
|
39
|
+
user: PersistedUser;
|
|
40
|
+
subscription: PersistedSubscription;
|
|
41
|
+
/** local user id -> qpush_id, for users referenced by pending operations. */
|
|
42
|
+
idMap: Record<string, string>;
|
|
43
|
+
consentRequired: boolean;
|
|
44
|
+
consentGiven: boolean;
|
|
45
|
+
/** Last FCM params from android_params.js (`null` = server has none). */
|
|
46
|
+
fcmParams?: FcmParams | null;
|
|
47
|
+
}
|
|
48
|
+
export declare function createUser(localId: string): PersistedUser;
|
|
49
|
+
export declare function createDefaultState(localId: string): PersistedState;
|
|
50
|
+
/** Validates persisted JSON, falling back to defaults for broken parts. */
|
|
51
|
+
export declare function sanitizeState(raw: unknown, newLocalId: () => string): PersistedState;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createUser = createUser;
|
|
4
|
+
exports.createDefaultState = createDefaultState;
|
|
5
|
+
exports.sanitizeState = sanitizeState;
|
|
6
|
+
function createUser(localId) {
|
|
7
|
+
return { localId, aliases: {}, tags: {} };
|
|
8
|
+
}
|
|
9
|
+
function createDefaultState(localId) {
|
|
10
|
+
return {
|
|
11
|
+
version: 1,
|
|
12
|
+
user: createUser(localId),
|
|
13
|
+
subscription: { optedIn: true, permission: 'notDetermined' },
|
|
14
|
+
idMap: {},
|
|
15
|
+
consentRequired: false,
|
|
16
|
+
consentGiven: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function isRecordOfStrings(value) {
|
|
20
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
21
|
+
return false;
|
|
22
|
+
return Object.values(value).every((entry) => typeof entry === 'string');
|
|
23
|
+
}
|
|
24
|
+
const PERMISSIONS = ['notDetermined', 'denied', 'granted', 'provisional', 'ephemeral'];
|
|
25
|
+
/** Validates persisted JSON, falling back to defaults for broken parts. */
|
|
26
|
+
function sanitizeState(raw, newLocalId) {
|
|
27
|
+
const fallback = createDefaultState(newLocalId());
|
|
28
|
+
if (typeof raw !== 'object' || raw === null)
|
|
29
|
+
return fallback;
|
|
30
|
+
const value = raw;
|
|
31
|
+
if (value.version !== 1)
|
|
32
|
+
return fallback;
|
|
33
|
+
const state = { ...fallback };
|
|
34
|
+
if (value.config &&
|
|
35
|
+
typeof value.config.appId === 'string' &&
|
|
36
|
+
typeof value.config.serverUrl === 'string') {
|
|
37
|
+
state.config = { appId: value.config.appId, serverUrl: value.config.serverUrl };
|
|
38
|
+
}
|
|
39
|
+
const user = value.user;
|
|
40
|
+
if (user && typeof user.localId === 'string') {
|
|
41
|
+
state.user = {
|
|
42
|
+
localId: user.localId,
|
|
43
|
+
qpushId: typeof user.qpushId === 'string' ? user.qpushId : undefined,
|
|
44
|
+
externalId: typeof user.externalId === 'string' ? user.externalId : undefined,
|
|
45
|
+
aliases: isRecordOfStrings(user.aliases) ? user.aliases : {},
|
|
46
|
+
tags: isRecordOfStrings(user.tags) ? user.tags : {},
|
|
47
|
+
language: typeof user.language === 'string' ? user.language : undefined,
|
|
48
|
+
languageExplicit: user.languageExplicit === true,
|
|
49
|
+
timezone: typeof user.timezone === 'string' ? user.timezone : undefined,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const subscription = value.subscription;
|
|
53
|
+
if (subscription && typeof subscription === 'object') {
|
|
54
|
+
state.subscription = {
|
|
55
|
+
id: typeof subscription.id === 'string' ? subscription.id : undefined,
|
|
56
|
+
token: typeof subscription.token === 'string' ? subscription.token : undefined,
|
|
57
|
+
tokenError: typeof subscription.tokenError === 'number' ? subscription.tokenError : undefined,
|
|
58
|
+
optedIn: subscription.optedIn !== false,
|
|
59
|
+
permission: PERMISSIONS.includes(subscription.permission)
|
|
60
|
+
? subscription.permission
|
|
61
|
+
: 'notDetermined',
|
|
62
|
+
synced: typeof subscription.synced === 'object' && subscription.synced !== null ? subscription.synced : undefined,
|
|
63
|
+
badgeResetPending: subscription.badgeResetPending === true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
state.idMap = isRecordOfStrings(value.idMap) ? value.idMap : {};
|
|
67
|
+
state.consentRequired = value.consentRequired === true;
|
|
68
|
+
state.consentGiven = value.consentGiven === true;
|
|
69
|
+
if (value.fcmParams === null)
|
|
70
|
+
state.fcmParams = null;
|
|
71
|
+
else if (value.fcmParams && typeof value.fcmParams === 'object')
|
|
72
|
+
state.fcmParams = value.fcmParams;
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal async key/value store. In the app it is backed by the native module
|
|
3
|
+
* (SharedPreferences on Android, UserDefaults on iOS) so native code can read
|
|
4
|
+
* the same values; tests use {@link MemoryStore}.
|
|
5
|
+
*/
|
|
6
|
+
export interface KeyValueStore {
|
|
7
|
+
getItem(key: string): Promise<string | null>;
|
|
8
|
+
setItem(key: string, value: string): Promise<void>;
|
|
9
|
+
removeItem(key: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare class MemoryStore implements KeyValueStore {
|
|
12
|
+
readonly data: Map<string, string>;
|
|
13
|
+
getItem(key: string): Promise<string | null>;
|
|
14
|
+
setItem(key: string, value: string): Promise<void>;
|
|
15
|
+
removeItem(key: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** Reads and parses a JSON value, returning `null` on any failure. */
|
|
18
|
+
export declare function readJson<T>(store: KeyValueStore, key: string): Promise<T | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Serializes writes for one key: only the latest value matters, writes never
|
|
21
|
+
* overlap and a failed write does not break the chain.
|
|
22
|
+
*/
|
|
23
|
+
export declare class SerialWriter {
|
|
24
|
+
private readonly store;
|
|
25
|
+
private readonly key;
|
|
26
|
+
private readonly onError;
|
|
27
|
+
private chain;
|
|
28
|
+
private latest;
|
|
29
|
+
private scheduled;
|
|
30
|
+
constructor(store: KeyValueStore, key: string, onError?: (error: unknown) => void);
|
|
31
|
+
write(value: string): Promise<void>;
|
|
32
|
+
/** Resolves when every pending write finished. */
|
|
33
|
+
flush(): Promise<void>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SerialWriter = exports.MemoryStore = void 0;
|
|
4
|
+
exports.readJson = readJson;
|
|
5
|
+
class MemoryStore {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.data = new Map();
|
|
8
|
+
}
|
|
9
|
+
async getItem(key) {
|
|
10
|
+
return this.data.has(key) ? this.data.get(key) : null;
|
|
11
|
+
}
|
|
12
|
+
async setItem(key, value) {
|
|
13
|
+
this.data.set(key, value);
|
|
14
|
+
}
|
|
15
|
+
async removeItem(key) {
|
|
16
|
+
this.data.delete(key);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.MemoryStore = MemoryStore;
|
|
20
|
+
/** Reads and parses a JSON value, returning `null` on any failure. */
|
|
21
|
+
async function readJson(store, key) {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await store.getItem(key);
|
|
24
|
+
if (raw == null || raw === '')
|
|
25
|
+
return null;
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Serializes writes for one key: only the latest value matters, writes never
|
|
34
|
+
* overlap and a failed write does not break the chain.
|
|
35
|
+
*/
|
|
36
|
+
class SerialWriter {
|
|
37
|
+
constructor(store, key, onError = () => { }) {
|
|
38
|
+
this.store = store;
|
|
39
|
+
this.key = key;
|
|
40
|
+
this.onError = onError;
|
|
41
|
+
this.chain = Promise.resolve();
|
|
42
|
+
this.latest = null;
|
|
43
|
+
this.scheduled = false;
|
|
44
|
+
}
|
|
45
|
+
write(value) {
|
|
46
|
+
this.latest = value;
|
|
47
|
+
if (!this.scheduled) {
|
|
48
|
+
this.scheduled = true;
|
|
49
|
+
this.chain = this.chain
|
|
50
|
+
.then(async () => {
|
|
51
|
+
this.scheduled = false;
|
|
52
|
+
const toWrite = this.latest;
|
|
53
|
+
if (toWrite !== null)
|
|
54
|
+
await this.store.setItem(this.key, toWrite);
|
|
55
|
+
})
|
|
56
|
+
.catch((error) => {
|
|
57
|
+
this.scheduled = false;
|
|
58
|
+
this.onError(error);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return this.chain;
|
|
62
|
+
}
|
|
63
|
+
/** Resolves when every pending write finished. */
|
|
64
|
+
flush() {
|
|
65
|
+
return this.chain;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.SerialWriter = SerialWriter;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { PermissionStatus, SubscriptionPayload } from '../types';
|
|
2
|
+
/** Mutable subscription fields that the SDK keeps in sync with the server. */
|
|
3
|
+
export interface SubscriptionFields {
|
|
4
|
+
token?: string;
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
notification_types: number;
|
|
7
|
+
device_model?: string;
|
|
8
|
+
device_os?: string;
|
|
9
|
+
sdk: string;
|
|
10
|
+
app_version?: string;
|
|
11
|
+
test_type?: number | null;
|
|
12
|
+
carrier?: string;
|
|
13
|
+
rooted?: boolean;
|
|
14
|
+
net_type?: number;
|
|
15
|
+
}
|
|
16
|
+
export type SubscriptionPatch = Partial<SubscriptionFields> & {
|
|
17
|
+
badge_count?: number;
|
|
18
|
+
};
|
|
19
|
+
/** Device information reported by the native module. */
|
|
20
|
+
export interface DeviceInfo {
|
|
21
|
+
platform: 'ios' | 'android';
|
|
22
|
+
deviceModel?: string;
|
|
23
|
+
deviceOs?: string;
|
|
24
|
+
appVersion?: string;
|
|
25
|
+
/** IANA timezone id. */
|
|
26
|
+
timezone?: string;
|
|
27
|
+
/** BCP-47 locale tag (e.g. `pt-BR`). */
|
|
28
|
+
locale?: string;
|
|
29
|
+
/** ISO 639-1 language (e.g. `pt`). */
|
|
30
|
+
language?: string;
|
|
31
|
+
/** ISO 3166-1 alpha-2 country from the locale. */
|
|
32
|
+
country?: string;
|
|
33
|
+
carrier?: string;
|
|
34
|
+
rooted?: boolean;
|
|
35
|
+
netType?: number;
|
|
36
|
+
/** iOS only: 1 development, 2 ad-hoc, null App Store/TestFlight. */
|
|
37
|
+
testType?: number | null;
|
|
38
|
+
}
|
|
39
|
+
export interface SubscriptionInputs {
|
|
40
|
+
token?: string | null;
|
|
41
|
+
/** Negative `notification_types` reported by the native registration. */
|
|
42
|
+
tokenError?: number | null;
|
|
43
|
+
permission: PermissionStatus;
|
|
44
|
+
/** Developer intent (`optIn()` / `optOut()`), default `true`. */
|
|
45
|
+
optedIn: boolean;
|
|
46
|
+
}
|
|
47
|
+
export declare function isPermissionGranted(permission: PermissionStatus): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Computes `notification_types`:
|
|
50
|
+
* token errors (-3/-10) > opted out / denied (-2) > not asked (0) > subscribed (1).
|
|
51
|
+
*/
|
|
52
|
+
export declare function computeNotificationTypes(input: SubscriptionInputs): number;
|
|
53
|
+
/** `enabled` is false when opted out or when the OS permission is not granted. */
|
|
54
|
+
export declare function computeEnabled(input: SubscriptionInputs): boolean;
|
|
55
|
+
/** What `pushSubscription.getOptedInAsync()` reports. */
|
|
56
|
+
export declare function computeOptedIn(input: SubscriptionInputs): boolean;
|
|
57
|
+
/** Builds the full subscription field set from local state and device info. */
|
|
58
|
+
export declare function buildSubscriptionFields(input: SubscriptionInputs, device: DeviceInfo | null): SubscriptionFields;
|
|
59
|
+
export declare function subscriptionType(platform: 'ios' | 'android'): SubscriptionPayload['type'];
|
|
60
|
+
/** Full payload for `POST /users` / `POST .../subscriptions`. */
|
|
61
|
+
export declare function toSubscriptionPayload(platform: 'ios' | 'android', fields: SubscriptionFields, id?: string): SubscriptionPayload;
|
|
62
|
+
/**
|
|
63
|
+
* Returns the fields of `current` that differ from what the server last
|
|
64
|
+
* acknowledged (`synced`). `net_type` alone never triggers a PATCH, it is only
|
|
65
|
+
* sent along with other changes (it flips constantly).
|
|
66
|
+
*/
|
|
67
|
+
export declare function diffSubscription(synced: Partial<SubscriptionFields> | undefined, current: SubscriptionFields): Partial<SubscriptionFields>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPermissionGranted = isPermissionGranted;
|
|
4
|
+
exports.computeNotificationTypes = computeNotificationTypes;
|
|
5
|
+
exports.computeEnabled = computeEnabled;
|
|
6
|
+
exports.computeOptedIn = computeOptedIn;
|
|
7
|
+
exports.buildSubscriptionFields = buildSubscriptionFields;
|
|
8
|
+
exports.subscriptionType = subscriptionType;
|
|
9
|
+
exports.toSubscriptionPayload = toSubscriptionPayload;
|
|
10
|
+
exports.diffSubscription = diffSubscription;
|
|
11
|
+
const constants_1 = require("./constants");
|
|
12
|
+
function isPermissionGranted(permission) {
|
|
13
|
+
return permission === 'granted' || permission === 'provisional' || permission === 'ephemeral';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Computes `notification_types`:
|
|
17
|
+
* token errors (-3/-10) > opted out / denied (-2) > not asked (0) > subscribed (1).
|
|
18
|
+
*/
|
|
19
|
+
function computeNotificationTypes(input) {
|
|
20
|
+
if (typeof input.tokenError === 'number' && input.tokenError < 0)
|
|
21
|
+
return input.tokenError;
|
|
22
|
+
if (!input.optedIn)
|
|
23
|
+
return constants_1.NotificationTypes.unsubscribed;
|
|
24
|
+
if (input.permission === 'denied')
|
|
25
|
+
return constants_1.NotificationTypes.unsubscribed;
|
|
26
|
+
if (input.permission === 'notDetermined')
|
|
27
|
+
return constants_1.NotificationTypes.notAsked;
|
|
28
|
+
if (!input.token)
|
|
29
|
+
return constants_1.NotificationTypes.notAsked;
|
|
30
|
+
return constants_1.NotificationTypes.subscribed;
|
|
31
|
+
}
|
|
32
|
+
/** `enabled` is false when opted out or when the OS permission is not granted. */
|
|
33
|
+
function computeEnabled(input) {
|
|
34
|
+
return input.optedIn && isPermissionGranted(input.permission);
|
|
35
|
+
}
|
|
36
|
+
/** What `pushSubscription.getOptedInAsync()` reports. */
|
|
37
|
+
function computeOptedIn(input) {
|
|
38
|
+
return input.optedIn && isPermissionGranted(input.permission) && !!input.token && !input.tokenError;
|
|
39
|
+
}
|
|
40
|
+
/** Builds the full subscription field set from local state and device info. */
|
|
41
|
+
function buildSubscriptionFields(input, device) {
|
|
42
|
+
var _a;
|
|
43
|
+
const fields = {
|
|
44
|
+
enabled: computeEnabled(input),
|
|
45
|
+
notification_types: computeNotificationTypes(input),
|
|
46
|
+
sdk: constants_1.SDK_VERSION,
|
|
47
|
+
};
|
|
48
|
+
if (input.token)
|
|
49
|
+
fields.token = input.token;
|
|
50
|
+
if (device) {
|
|
51
|
+
if (device.deviceModel)
|
|
52
|
+
fields.device_model = device.deviceModel;
|
|
53
|
+
if (device.deviceOs)
|
|
54
|
+
fields.device_os = device.deviceOs;
|
|
55
|
+
if (device.appVersion)
|
|
56
|
+
fields.app_version = device.appVersion;
|
|
57
|
+
if (device.carrier)
|
|
58
|
+
fields.carrier = device.carrier;
|
|
59
|
+
if (typeof device.rooted === 'boolean')
|
|
60
|
+
fields.rooted = device.rooted;
|
|
61
|
+
if (typeof device.netType === 'number')
|
|
62
|
+
fields.net_type = device.netType;
|
|
63
|
+
if (device.platform === 'ios')
|
|
64
|
+
fields.test_type = (_a = device.testType) !== null && _a !== void 0 ? _a : null;
|
|
65
|
+
}
|
|
66
|
+
return fields;
|
|
67
|
+
}
|
|
68
|
+
function subscriptionType(platform) {
|
|
69
|
+
return platform === 'ios' ? 'iOSPush' : 'AndroidPush';
|
|
70
|
+
}
|
|
71
|
+
/** Full payload for `POST /users` / `POST .../subscriptions`. */
|
|
72
|
+
function toSubscriptionPayload(platform, fields, id) {
|
|
73
|
+
const payload = { type: subscriptionType(platform), ...fields };
|
|
74
|
+
if (id)
|
|
75
|
+
payload.id = id;
|
|
76
|
+
return payload;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns the fields of `current` that differ from what the server last
|
|
80
|
+
* acknowledged (`synced`). `net_type` alone never triggers a PATCH, it is only
|
|
81
|
+
* sent along with other changes (it flips constantly).
|
|
82
|
+
*/
|
|
83
|
+
function diffSubscription(synced, current) {
|
|
84
|
+
const previous = synced !== null && synced !== void 0 ? synced : {};
|
|
85
|
+
const patch = {};
|
|
86
|
+
const keys = Object.keys(current);
|
|
87
|
+
for (const key of keys) {
|
|
88
|
+
const value = current[key];
|
|
89
|
+
if (value === undefined)
|
|
90
|
+
continue;
|
|
91
|
+
if (previous[key] !== value)
|
|
92
|
+
patch[key] = value;
|
|
93
|
+
}
|
|
94
|
+
const changedKeys = Object.keys(patch);
|
|
95
|
+
if (changedKeys.length === 1 && changedKeys[0] === 'net_type')
|
|
96
|
+
return {};
|
|
97
|
+
return patch;
|
|
98
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag helpers. Tags are always strings on the wire; an empty string value in
|
|
3
|
+
* a PATCH removes the tag (protocol: "Tags are merged; an empty-string value
|
|
4
|
+
* removes the tag").
|
|
5
|
+
*/
|
|
6
|
+
export type TagMap = Record<string, string>;
|
|
7
|
+
/** Changes to send: `value` sets, `''` removes. */
|
|
8
|
+
export type TagChanges = Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Converts a developer supplied tag value to the wire string.
|
|
11
|
+
* Returns `undefined` for unsupported values (objects, arrays, functions).
|
|
12
|
+
* `null`/`undefined` map to `''` (removal).
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeTagValue(value: unknown): string | undefined;
|
|
15
|
+
/** Returns `true` when the key can be used as a tag key. */
|
|
16
|
+
export declare function isValidTagKey(key: unknown): key is string;
|
|
17
|
+
/**
|
|
18
|
+
* Normalizes a tag object, dropping invalid keys and values.
|
|
19
|
+
* The second element lists the keys that were rejected.
|
|
20
|
+
*/
|
|
21
|
+
export declare function normalizeTags(input: Record<string, unknown>): [TagChanges, string[]];
|
|
22
|
+
/**
|
|
23
|
+
* Computes the changes that turn `previous` into `next`.
|
|
24
|
+
* Changed / added keys map to their new value, removed keys map to `''`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function diffTags(previous: TagMap, next: TagMap): TagChanges;
|
|
27
|
+
/** Applies changes to a tag map (returns a new object). */
|
|
28
|
+
export declare function applyTagChanges(base: TagMap, changes: TagChanges): TagMap;
|
|
29
|
+
/** Merges two change sets; the later one wins per key. */
|
|
30
|
+
export declare function mergeTagChanges(first: TagChanges, second: TagChanges): TagChanges;
|
|
31
|
+
/**
|
|
32
|
+
* Computes what must be sent for an `addTags` call given the local cache:
|
|
33
|
+
* only values that differ from the cache are sent. Empty values are removals
|
|
34
|
+
* and are always sent (the local cache may not know every server tag).
|
|
35
|
+
*/
|
|
36
|
+
export declare function changesForAdd(current: TagMap, additions: TagChanges): TagChanges;
|
|
37
|
+
/** Computes the removal change set for `removeTags` (always sent). */
|
|
38
|
+
export declare function changesForRemove(keys: readonly string[]): TagChanges;
|
|
39
|
+
export declare function isEmpty(changes: Record<string, unknown> | undefined | null): boolean;
|
package/lib/core/tags.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tag helpers. Tags are always strings on the wire; an empty string value in
|
|
4
|
+
* a PATCH removes the tag (protocol: "Tags are merged; an empty-string value
|
|
5
|
+
* removes the tag").
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.normalizeTagValue = normalizeTagValue;
|
|
9
|
+
exports.isValidTagKey = isValidTagKey;
|
|
10
|
+
exports.normalizeTags = normalizeTags;
|
|
11
|
+
exports.diffTags = diffTags;
|
|
12
|
+
exports.applyTagChanges = applyTagChanges;
|
|
13
|
+
exports.mergeTagChanges = mergeTagChanges;
|
|
14
|
+
exports.changesForAdd = changesForAdd;
|
|
15
|
+
exports.changesForRemove = changesForRemove;
|
|
16
|
+
exports.isEmpty = isEmpty;
|
|
17
|
+
/**
|
|
18
|
+
* Converts a developer supplied tag value to the wire string.
|
|
19
|
+
* Returns `undefined` for unsupported values (objects, arrays, functions).
|
|
20
|
+
* `null`/`undefined` map to `''` (removal).
|
|
21
|
+
*/
|
|
22
|
+
function normalizeTagValue(value) {
|
|
23
|
+
if (value === null || value === undefined)
|
|
24
|
+
return '';
|
|
25
|
+
switch (typeof value) {
|
|
26
|
+
case 'string':
|
|
27
|
+
return value;
|
|
28
|
+
case 'number':
|
|
29
|
+
return Number.isFinite(value) ? String(value) : undefined;
|
|
30
|
+
case 'boolean':
|
|
31
|
+
case 'bigint':
|
|
32
|
+
return String(value);
|
|
33
|
+
default:
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Returns `true` when the key can be used as a tag key. */
|
|
38
|
+
function isValidTagKey(key) {
|
|
39
|
+
return typeof key === 'string' && key.trim().length > 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Normalizes a tag object, dropping invalid keys and values.
|
|
43
|
+
* The second element lists the keys that were rejected.
|
|
44
|
+
*/
|
|
45
|
+
function normalizeTags(input) {
|
|
46
|
+
const result = {};
|
|
47
|
+
const rejected = [];
|
|
48
|
+
if (!input || typeof input !== 'object')
|
|
49
|
+
return [result, rejected];
|
|
50
|
+
for (const key of Object.keys(input)) {
|
|
51
|
+
const value = normalizeTagValue(input[key]);
|
|
52
|
+
if (!isValidTagKey(key) || value === undefined) {
|
|
53
|
+
rejected.push(key);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
result[key] = value;
|
|
57
|
+
}
|
|
58
|
+
return [result, rejected];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Computes the changes that turn `previous` into `next`.
|
|
62
|
+
* Changed / added keys map to their new value, removed keys map to `''`.
|
|
63
|
+
*/
|
|
64
|
+
function diffTags(previous, next) {
|
|
65
|
+
const changes = {};
|
|
66
|
+
for (const key of Object.keys(next)) {
|
|
67
|
+
const value = next[key];
|
|
68
|
+
if (value === '') {
|
|
69
|
+
if (key in previous)
|
|
70
|
+
changes[key] = '';
|
|
71
|
+
}
|
|
72
|
+
else if (previous[key] !== value) {
|
|
73
|
+
changes[key] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const key of Object.keys(previous)) {
|
|
77
|
+
if (!(key in next))
|
|
78
|
+
changes[key] = '';
|
|
79
|
+
}
|
|
80
|
+
return changes;
|
|
81
|
+
}
|
|
82
|
+
/** Applies changes to a tag map (returns a new object). */
|
|
83
|
+
function applyTagChanges(base, changes) {
|
|
84
|
+
const result = { ...base };
|
|
85
|
+
for (const key of Object.keys(changes)) {
|
|
86
|
+
const value = changes[key];
|
|
87
|
+
if (value === '')
|
|
88
|
+
delete result[key];
|
|
89
|
+
else
|
|
90
|
+
result[key] = value;
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
/** Merges two change sets; the later one wins per key. */
|
|
95
|
+
function mergeTagChanges(first, second) {
|
|
96
|
+
return { ...first, ...second };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Computes what must be sent for an `addTags` call given the local cache:
|
|
100
|
+
* only values that differ from the cache are sent. Empty values are removals
|
|
101
|
+
* and are always sent (the local cache may not know every server tag).
|
|
102
|
+
*/
|
|
103
|
+
function changesForAdd(current, additions) {
|
|
104
|
+
const changes = {};
|
|
105
|
+
for (const key of Object.keys(additions)) {
|
|
106
|
+
const value = additions[key];
|
|
107
|
+
if (value === '' || current[key] !== value)
|
|
108
|
+
changes[key] = value;
|
|
109
|
+
}
|
|
110
|
+
return changes;
|
|
111
|
+
}
|
|
112
|
+
/** Computes the removal change set for `removeTags` (always sent). */
|
|
113
|
+
function changesForRemove(keys) {
|
|
114
|
+
const changes = {};
|
|
115
|
+
for (const key of keys) {
|
|
116
|
+
if (isValidTagKey(key))
|
|
117
|
+
changes[key] = '';
|
|
118
|
+
}
|
|
119
|
+
return changes;
|
|
120
|
+
}
|
|
121
|
+
function isEmpty(changes) {
|
|
122
|
+
return !changes || Object.keys(changes).length === 0;
|
|
123
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL helpers. React Native's `URL` polyfill is incomplete (several getters
|
|
3
|
+
* throw "not implemented"), so everything here is plain string handling.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes the server URL given to `QPush.initialize`:
|
|
7
|
+
* - trims whitespace, drops query string / fragment and trailing slashes
|
|
8
|
+
* - prepends `https://` when no scheme is given
|
|
9
|
+
* - appends `/api/v1` when the path does not already end with it
|
|
10
|
+
*
|
|
11
|
+
* Returns `null` when the value cannot be used.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeServerUrl(input: string | null | undefined): string | null;
|
|
14
|
+
/** Joins the API base URL with an absolute API path (`/apps/...`). */
|
|
15
|
+
export declare function joinUrl(base: string, path: string): string;
|
|
16
|
+
/** Encodes a single path segment (alias ids, labels, ids). */
|
|
17
|
+
export declare function encodeSegment(segment: string): string;
|