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,133 @@
1
+ import type { PermissionStatus, SubscriptionPayload } from '../types';
2
+ import { NotificationTypes, SDK_VERSION } from './constants';
3
+
4
+ /** Mutable subscription fields that the SDK keeps in sync with the server. */
5
+ export interface SubscriptionFields {
6
+ token?: string;
7
+ enabled: boolean;
8
+ notification_types: number;
9
+ device_model?: string;
10
+ device_os?: string;
11
+ sdk: string;
12
+ app_version?: string;
13
+ test_type?: number | null;
14
+ carrier?: string;
15
+ rooted?: boolean;
16
+ net_type?: number;
17
+ }
18
+
19
+ export type SubscriptionPatch = Partial<SubscriptionFields> & { badge_count?: number };
20
+
21
+ /** Device information reported by the native module. */
22
+ export interface DeviceInfo {
23
+ platform: 'ios' | 'android';
24
+ deviceModel?: string;
25
+ deviceOs?: string;
26
+ appVersion?: string;
27
+ /** IANA timezone id. */
28
+ timezone?: string;
29
+ /** BCP-47 locale tag (e.g. `pt-BR`). */
30
+ locale?: string;
31
+ /** ISO 639-1 language (e.g. `pt`). */
32
+ language?: string;
33
+ /** ISO 3166-1 alpha-2 country from the locale. */
34
+ country?: string;
35
+ carrier?: string;
36
+ rooted?: boolean;
37
+ netType?: number;
38
+ /** iOS only: 1 development, 2 ad-hoc, null App Store/TestFlight. */
39
+ testType?: number | null;
40
+ }
41
+
42
+ export interface SubscriptionInputs {
43
+ token?: string | null;
44
+ /** Negative `notification_types` reported by the native registration. */
45
+ tokenError?: number | null;
46
+ permission: PermissionStatus;
47
+ /** Developer intent (`optIn()` / `optOut()`), default `true`. */
48
+ optedIn: boolean;
49
+ }
50
+
51
+ export function isPermissionGranted(permission: PermissionStatus): boolean {
52
+ return permission === 'granted' || permission === 'provisional' || permission === 'ephemeral';
53
+ }
54
+
55
+ /**
56
+ * Computes `notification_types`:
57
+ * token errors (-3/-10) > opted out / denied (-2) > not asked (0) > subscribed (1).
58
+ */
59
+ export function computeNotificationTypes(input: SubscriptionInputs): number {
60
+ if (typeof input.tokenError === 'number' && input.tokenError < 0) return input.tokenError;
61
+ if (!input.optedIn) return NotificationTypes.unsubscribed;
62
+ if (input.permission === 'denied') return NotificationTypes.unsubscribed;
63
+ if (input.permission === 'notDetermined') return NotificationTypes.notAsked;
64
+ if (!input.token) return NotificationTypes.notAsked;
65
+ return NotificationTypes.subscribed;
66
+ }
67
+
68
+ /** `enabled` is false when opted out or when the OS permission is not granted. */
69
+ export function computeEnabled(input: SubscriptionInputs): boolean {
70
+ return input.optedIn && isPermissionGranted(input.permission);
71
+ }
72
+
73
+ /** What `pushSubscription.getOptedInAsync()` reports. */
74
+ export function computeOptedIn(input: SubscriptionInputs): boolean {
75
+ return input.optedIn && isPermissionGranted(input.permission) && !!input.token && !input.tokenError;
76
+ }
77
+
78
+ /** Builds the full subscription field set from local state and device info. */
79
+ export function buildSubscriptionFields(input: SubscriptionInputs, device: DeviceInfo | null): SubscriptionFields {
80
+ const fields: SubscriptionFields = {
81
+ enabled: computeEnabled(input),
82
+ notification_types: computeNotificationTypes(input),
83
+ sdk: SDK_VERSION,
84
+ };
85
+ if (input.token) fields.token = input.token;
86
+ if (device) {
87
+ if (device.deviceModel) fields.device_model = device.deviceModel;
88
+ if (device.deviceOs) fields.device_os = device.deviceOs;
89
+ if (device.appVersion) fields.app_version = device.appVersion;
90
+ if (device.carrier) fields.carrier = device.carrier;
91
+ if (typeof device.rooted === 'boolean') fields.rooted = device.rooted;
92
+ if (typeof device.netType === 'number') fields.net_type = device.netType;
93
+ if (device.platform === 'ios') fields.test_type = device.testType ?? null;
94
+ }
95
+ return fields;
96
+ }
97
+
98
+ export function subscriptionType(platform: 'ios' | 'android'): SubscriptionPayload['type'] {
99
+ return platform === 'ios' ? 'iOSPush' : 'AndroidPush';
100
+ }
101
+
102
+ /** Full payload for `POST /users` / `POST .../subscriptions`. */
103
+ export function toSubscriptionPayload(
104
+ platform: 'ios' | 'android',
105
+ fields: SubscriptionFields,
106
+ id?: string,
107
+ ): SubscriptionPayload {
108
+ const payload: SubscriptionPayload = { type: subscriptionType(platform), ...fields };
109
+ if (id) payload.id = id;
110
+ return payload;
111
+ }
112
+
113
+ /**
114
+ * Returns the fields of `current` that differ from what the server last
115
+ * acknowledged (`synced`). `net_type` alone never triggers a PATCH, it is only
116
+ * sent along with other changes (it flips constantly).
117
+ */
118
+ export function diffSubscription(
119
+ synced: Partial<SubscriptionFields> | undefined,
120
+ current: SubscriptionFields,
121
+ ): Partial<SubscriptionFields> {
122
+ const previous = synced ?? {};
123
+ const patch: Record<string, unknown> = {};
124
+ const keys = Object.keys(current) as (keyof SubscriptionFields)[];
125
+ for (const key of keys) {
126
+ const value = current[key];
127
+ if (value === undefined) continue;
128
+ if (previous[key] !== value) patch[key] = value;
129
+ }
130
+ const changedKeys = Object.keys(patch);
131
+ if (changedKeys.length === 1 && changedKeys[0] === 'net_type') return {};
132
+ return patch as Partial<SubscriptionFields>;
133
+ }
@@ -0,0 +1,116 @@
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
+
7
+ export type TagMap = Record<string, string>;
8
+ /** Changes to send: `value` sets, `''` removes. */
9
+ export type TagChanges = Record<string, string>;
10
+
11
+ /**
12
+ * Converts a developer supplied tag value to the wire string.
13
+ * Returns `undefined` for unsupported values (objects, arrays, functions).
14
+ * `null`/`undefined` map to `''` (removal).
15
+ */
16
+ export function normalizeTagValue(value: unknown): string | undefined {
17
+ if (value === null || value === undefined) return '';
18
+ switch (typeof value) {
19
+ case 'string':
20
+ return value;
21
+ case 'number':
22
+ return Number.isFinite(value) ? String(value) : undefined;
23
+ case 'boolean':
24
+ case 'bigint':
25
+ return String(value);
26
+ default:
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ /** Returns `true` when the key can be used as a tag key. */
32
+ export function isValidTagKey(key: unknown): key is string {
33
+ return typeof key === 'string' && key.trim().length > 0;
34
+ }
35
+
36
+ /**
37
+ * Normalizes a tag object, dropping invalid keys and values.
38
+ * The second element lists the keys that were rejected.
39
+ */
40
+ export function normalizeTags(input: Record<string, unknown>): [TagChanges, string[]] {
41
+ const result: TagChanges = {};
42
+ const rejected: string[] = [];
43
+ if (!input || typeof input !== 'object') return [result, rejected];
44
+ for (const key of Object.keys(input)) {
45
+ const value = normalizeTagValue(input[key]);
46
+ if (!isValidTagKey(key) || value === undefined) {
47
+ rejected.push(key);
48
+ continue;
49
+ }
50
+ result[key] = value;
51
+ }
52
+ return [result, rejected];
53
+ }
54
+
55
+ /**
56
+ * Computes the changes that turn `previous` into `next`.
57
+ * Changed / added keys map to their new value, removed keys map to `''`.
58
+ */
59
+ export function diffTags(previous: TagMap, next: TagMap): TagChanges {
60
+ const changes: TagChanges = {};
61
+ for (const key of Object.keys(next)) {
62
+ const value = next[key]!;
63
+ if (value === '') {
64
+ if (key in previous) changes[key] = '';
65
+ } else if (previous[key] !== value) {
66
+ changes[key] = value;
67
+ }
68
+ }
69
+ for (const key of Object.keys(previous)) {
70
+ if (!(key in next)) changes[key] = '';
71
+ }
72
+ return changes;
73
+ }
74
+
75
+ /** Applies changes to a tag map (returns a new object). */
76
+ export function applyTagChanges(base: TagMap, changes: TagChanges): TagMap {
77
+ const result: TagMap = { ...base };
78
+ for (const key of Object.keys(changes)) {
79
+ const value = changes[key]!;
80
+ if (value === '') delete result[key];
81
+ else result[key] = value;
82
+ }
83
+ return result;
84
+ }
85
+
86
+ /** Merges two change sets; the later one wins per key. */
87
+ export function mergeTagChanges(first: TagChanges, second: TagChanges): TagChanges {
88
+ return { ...first, ...second };
89
+ }
90
+
91
+ /**
92
+ * Computes what must be sent for an `addTags` call given the local cache:
93
+ * only values that differ from the cache are sent. Empty values are removals
94
+ * and are always sent (the local cache may not know every server tag).
95
+ */
96
+ export function changesForAdd(current: TagMap, additions: TagChanges): TagChanges {
97
+ const changes: TagChanges = {};
98
+ for (const key of Object.keys(additions)) {
99
+ const value = additions[key]!;
100
+ if (value === '' || current[key] !== value) changes[key] = value;
101
+ }
102
+ return changes;
103
+ }
104
+
105
+ /** Computes the removal change set for `removeTags` (always sent). */
106
+ export function changesForRemove(keys: readonly string[]): TagChanges {
107
+ const changes: TagChanges = {};
108
+ for (const key of keys) {
109
+ if (isValidTagKey(key)) changes[key] = '';
110
+ }
111
+ return changes;
112
+ }
113
+
114
+ export function isEmpty(changes: Record<string, unknown> | undefined | null): boolean {
115
+ return !changes || Object.keys(changes).length === 0;
116
+ }
@@ -0,0 +1,58 @@
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
+ const API_SUFFIX = '/api/v1';
7
+
8
+ /**
9
+ * Normalizes the server URL given to `QPush.initialize`:
10
+ * - trims whitespace, drops query string / fragment and trailing slashes
11
+ * - prepends `https://` when no scheme is given
12
+ * - appends `/api/v1` when the path does not already end with it
13
+ *
14
+ * Returns `null` when the value cannot be used.
15
+ */
16
+ export function normalizeServerUrl(input: string | null | undefined): string | null {
17
+ if (typeof input !== 'string') return null;
18
+ let url = input.trim();
19
+ if (url.length === 0) return null;
20
+
21
+ // Strip fragment and query string.
22
+ const hashIndex = url.indexOf('#');
23
+ if (hashIndex >= 0) url = url.slice(0, hashIndex);
24
+ const queryIndex = url.indexOf('?');
25
+ if (queryIndex >= 0) url = url.slice(0, queryIndex);
26
+
27
+ if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(url)) {
28
+ if (url.startsWith('//')) url = url.slice(2);
29
+ url = `https://${url}`;
30
+ }
31
+
32
+ const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/]*)(\/.*)?$/.exec(url);
33
+ if (!match) return null;
34
+ const scheme = match[1]!.toLowerCase();
35
+ const host = match[2]!;
36
+ if (scheme !== 'http' && scheme !== 'https') return null;
37
+ if (host.length === 0 || /\s/.test(host)) return null;
38
+
39
+ let path = (match[3] ?? '').replace(/\/+$/, '');
40
+ // Collapse duplicated slashes inside the path.
41
+ path = path.replace(/\/{2,}/g, '/');
42
+ if (!path.toLowerCase().endsWith(API_SUFFIX)) {
43
+ path = `${path}${API_SUFFIX}`;
44
+ }
45
+ return `${scheme}://${host}${path}`;
46
+ }
47
+
48
+ /** Joins the API base URL with an absolute API path (`/apps/...`). */
49
+ export function joinUrl(base: string, path: string): string {
50
+ const trimmedBase = base.replace(/\/+$/, '');
51
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
52
+ return `${trimmedBase}${normalizedPath}`;
53
+ }
54
+
55
+ /** Encodes a single path segment (alias ids, labels, ids). */
56
+ export function encodeSegment(segment: string): string {
57
+ return encodeURIComponent(segment);
58
+ }
@@ -0,0 +1,13 @@
1
+ /** RFC 4122 v4 UUID (used for local ids only, not security sensitive). */
2
+ export function generateUuid(random: () => number = Math.random): string {
3
+ const hex: string[] = [];
4
+ for (let i = 0; i < 16; i++) {
5
+ let byte = Math.floor(random() * 256) & 0xff;
6
+ if (i === 6) byte = (byte & 0x0f) | 0x40;
7
+ if (i === 8) byte = (byte & 0x3f) | 0x80;
8
+ hex.push(byte.toString(16).padStart(2, '0'));
9
+ }
10
+ return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex
11
+ .slice(8, 10)
12
+ .join('')}-${hex.slice(10, 16).join('')}`;
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ export { QPush, QPush as default } from './QPush';
2
+ export type {
3
+ QPushType,
4
+ NotificationEventName,
5
+ NotificationEventTypeMap,
6
+ PushSubscriptionEventTypeMap,
7
+ UserEventTypeMap,
8
+ } from './QPush';
9
+ export { QPushNotification, NotificationWillDisplayEvent } from './notification';
10
+ export type { NotificationClickEvent } from './notification';
11
+ export { LogLevel, NotificationPermission, QPUSH_CLOUD_API_URL } from './types';
12
+ export type {
13
+ ActionButton,
14
+ QPushInitOptions,
15
+ QPushNotificationData,
16
+ NotificationClickResult,
17
+ PermissionStatus,
18
+ PushSubscriptionChangedState,
19
+ PushSubscriptionState,
20
+ UserChangedState,
21
+ UserState,
22
+ } from './types';
23
+ export { SDK_VERSION } from './core/constants';
@@ -0,0 +1,113 @@
1
+ import type { ActionButton, QPushNotificationData, NotificationClickResult } from './types';
2
+
3
+ /**
4
+ * A received notification. Same idea as OneSignal's `OSNotification`:
5
+ * read-only content plus `display()` for foreground notifications whose
6
+ * default display was prevented.
7
+ */
8
+ export class QPushNotification implements QPushNotificationData {
9
+ readonly notificationId!: string;
10
+ readonly title?: string;
11
+ readonly subtitle?: string;
12
+ readonly body?: string;
13
+ readonly launchURL?: string;
14
+ readonly additionalData?: Record<string, unknown>;
15
+ readonly bigPicture?: string;
16
+ readonly largeIcon?: string;
17
+ readonly smallIcon?: string;
18
+ readonly accentColor?: string;
19
+ readonly sound?: string;
20
+ readonly channelId?: string;
21
+ readonly priority?: number;
22
+ readonly groupKey?: string;
23
+ readonly collapseId?: string;
24
+ readonly visibility?: number;
25
+ readonly badge?: number;
26
+ readonly category?: string;
27
+ readonly interruptionLevel?: string;
28
+ readonly relevanceScore?: number;
29
+ readonly contentAvailable?: boolean;
30
+ readonly actionButtons?: ActionButton[];
31
+ readonly rawPayload!: Record<string, unknown>;
32
+
33
+ private readonly displayHandler?: () => void;
34
+
35
+ constructor(data: QPushNotificationData, displayHandler?: () => void) {
36
+ Object.assign(this, data);
37
+ Object.defineProperty(this, 'displayHandler', { value: displayHandler, enumerable: false });
38
+ }
39
+
40
+ /**
41
+ * Displays the notification. Only meaningful inside a
42
+ * `foregroundWillDisplay` listener after `event.preventDefault()`; a no-op
43
+ * otherwise.
44
+ */
45
+ display(): void {
46
+ this.displayHandler?.();
47
+ }
48
+
49
+ /** Plain object copy (safe to serialize / store in React state). */
50
+ toJSON(): QPushNotificationData {
51
+ const copy: Record<string, unknown> = {};
52
+ for (const key of Object.keys(this)) {
53
+ copy[key] = (this as unknown as Record<string, unknown>)[key];
54
+ }
55
+ return copy as unknown as QPushNotificationData;
56
+ }
57
+ }
58
+
59
+ /** Payload of the `click` event (mirrors OneSignal's `NotificationClickEvent`). */
60
+ export interface NotificationClickEvent {
61
+ result: NotificationClickResult;
62
+ notification: QPushNotification;
63
+ }
64
+
65
+ type DisplayState = 'pending' | 'prevented' | 'displayed';
66
+
67
+ /**
68
+ * Payload of the `foregroundWillDisplay` event. Call `preventDefault()`
69
+ * synchronously inside the listener to stop the notification from being
70
+ * shown; call `getNotification().display()` later to show it anyway.
71
+ */
72
+ export class NotificationWillDisplayEvent {
73
+ private state: DisplayState = 'pending';
74
+ private readonly notification: QPushNotification;
75
+
76
+ constructor(
77
+ data: QPushNotificationData,
78
+ private readonly onDisplay: () => void,
79
+ ) {
80
+ this.notification = new QPushNotification(data, () => this.display());
81
+ }
82
+
83
+ /** Prevents the default display. Must be called synchronously in the listener. */
84
+ preventDefault(): void {
85
+ if (this.state === 'pending') this.state = 'prevented';
86
+ }
87
+
88
+ getNotification(): QPushNotification {
89
+ return this.notification;
90
+ }
91
+
92
+ /** @internal */
93
+ get isPrevented(): boolean {
94
+ return this.state === 'prevented';
95
+ }
96
+
97
+ /** @internal */
98
+ get isDisplayed(): boolean {
99
+ return this.state === 'displayed';
100
+ }
101
+
102
+ private display(): void {
103
+ if (this.state === 'displayed') return;
104
+ this.state = 'displayed';
105
+ this.onDisplay();
106
+ }
107
+
108
+ /** @internal Called after all listeners ran: displays unless prevented. */
109
+ settle(): 'displayed' | 'prevented' {
110
+ if (this.state === 'pending') this.display();
111
+ return this.state === 'displayed' ? 'displayed' : 'prevented';
112
+ }
113
+ }
@@ -0,0 +1,172 @@
1
+ import { AppState, NativeEventEmitter, NativeModules, Platform } from 'react-native';
2
+ import NativeQPushPush, { type Spec } from './NativeQPushPush';
3
+ import type {
4
+ FetchLike,
5
+ NativeBridge,
6
+ NativeEventSource,
7
+ NativePushRegistration,
8
+ PlatformAdapter,
9
+ } from './core/platform';
10
+ import { defaultScheduler } from './core/platform';
11
+ import { MemoryStore } from './core/storage';
12
+ import type { DeviceInfo } from './core/subscription';
13
+ import type { FcmParams, PermissionStatus } from './types';
14
+
15
+ const LINKING_ERROR =
16
+ "react-native-qpush: the native module 'QPushPush' is not available. " +
17
+ 'Make sure you rebuilt the app after installing the package (pod install on iOS) ' +
18
+ 'and that you are not running in Expo Go (use a development build).';
19
+
20
+ const PERMISSIONS: PermissionStatus[] = ['notDetermined', 'denied', 'granted', 'provisional', 'ephemeral'];
21
+
22
+ function resolveNativeModule(): Spec | null {
23
+ let module: Spec | null = null;
24
+ try {
25
+ module = NativeQPushPush ?? null;
26
+ } catch {
27
+ module = null;
28
+ }
29
+ if (!module) module = (NativeModules.QPushPush as Spec | undefined) ?? null;
30
+ return module;
31
+ }
32
+
33
+ /** In-memory stand-in so JS keeps working (and logs) when the module is missing. */
34
+ function createFallbackBridge(os: 'ios' | 'android'): NativeBridge {
35
+ const store = new MemoryStore();
36
+ let warned = false;
37
+ const warn = () => {
38
+ if (!warned) {
39
+ warned = true;
40
+ console.error(`[QPush] ${LINKING_ERROR}`);
41
+ }
42
+ };
43
+ return {
44
+ getItem: (key) => store.getItem(key),
45
+ setItem: (key, value) => store.setItem(key, value),
46
+ removeItem: (key) => store.removeItem(key),
47
+ getDeviceInfo: async () => ({ platform: os }),
48
+ registerForPush: async () => {
49
+ warn();
50
+ return { token: null, error: 'native_module_missing' };
51
+ },
52
+ getPermissionStatus: async () => 'notDetermined',
53
+ requestPermission: async () => {
54
+ warn();
55
+ return false;
56
+ },
57
+ registerForProvisionalAuthorization: async () => false,
58
+ canRequestPermission: async () => false,
59
+ clearAll: warn,
60
+ removeNotification: warn,
61
+ removeGroupedNotifications: warn,
62
+ clearBadge: async () => 0,
63
+ setJsReady: () => {},
64
+ displayForegroundNotification: () => {},
65
+ preventForegroundDisplay: () => {},
66
+ setAutoOpenUrls: () => {},
67
+ setLogLevel: () => {},
68
+ beginBackgroundTask: async () => -1,
69
+ endBackgroundTask: () => {},
70
+ };
71
+ }
72
+
73
+ function toDeviceInfo(raw: unknown, os: 'ios' | 'android'): DeviceInfo {
74
+ const value = (raw ?? {}) as Record<string, unknown>;
75
+ const str = (key: string) => (typeof value[key] === 'string' && value[key] !== '' ? (value[key] as string) : undefined);
76
+ const info: DeviceInfo = { platform: os };
77
+ info.deviceModel = str('deviceModel');
78
+ info.deviceOs = str('deviceOs');
79
+ info.appVersion = str('appVersion');
80
+ info.timezone = str('timezone');
81
+ info.locale = str('locale');
82
+ info.language = str('language');
83
+ info.country = str('country');
84
+ info.carrier = str('carrier');
85
+ if (typeof value.rooted === 'boolean') info.rooted = value.rooted;
86
+ if (typeof value.netType === 'number' && value.netType >= 0) info.netType = value.netType;
87
+ if (os === 'ios') info.testType = typeof value.testType === 'number' ? value.testType : null;
88
+ return info;
89
+ }
90
+
91
+ function toRegistration(raw: unknown): NativePushRegistration {
92
+ const value = (raw ?? {}) as Record<string, unknown>;
93
+ const result: NativePushRegistration = {
94
+ token: typeof value.token === 'string' && value.token !== '' ? value.token : null,
95
+ };
96
+ if (typeof value.notificationTypes === 'number') result.notificationTypes = value.notificationTypes;
97
+ if (typeof value.error === 'string') result.error = value.error;
98
+ return result;
99
+ }
100
+
101
+ function wrapNative(module: Spec, os: 'ios' | 'android'): NativeBridge {
102
+ return {
103
+ getItem: (key) => module.getItem(key),
104
+ setItem: (key, value) => module.setItem(key, value),
105
+ removeItem: (key) => module.removeItem(key),
106
+ getDeviceInfo: async () => toDeviceInfo(await module.getDeviceInfo(), os),
107
+ registerForPush: async (fcm: FcmParams | null) =>
108
+ toRegistration(await module.registerForPush(fcm ? { ...fcm } : null)),
109
+ getPermissionStatus: async () => {
110
+ const status = await module.getPermissionStatus();
111
+ return PERMISSIONS.includes(status as PermissionStatus) ? (status as PermissionStatus) : 'notDetermined';
112
+ },
113
+ requestPermission: (fallback) => module.requestPermission(fallback),
114
+ registerForProvisionalAuthorization: () => module.registerForProvisionalAuthorization(),
115
+ canRequestPermission: () => module.canRequestPermission(),
116
+ clearAll: () => module.clearAll(),
117
+ removeNotification: (id) => module.removeNotification(id),
118
+ removeGroupedNotifications: (group) => module.removeGroupedNotifications(group),
119
+ clearBadge: async () => {
120
+ const previous = await module.clearBadge();
121
+ return typeof previous === 'number' ? previous : 0;
122
+ },
123
+ setJsReady: () => module.setJsReady(),
124
+ displayForegroundNotification: (id) => module.displayForegroundNotification(id),
125
+ preventForegroundDisplay: (id) => module.preventForegroundDisplay(id),
126
+ setAutoOpenUrls: (enabled) => module.setAutoOpenUrls(enabled),
127
+ setLogLevel: (level) => module.setLogLevel(level),
128
+ beginBackgroundTask: async () => {
129
+ const id = await module.beginBackgroundTask();
130
+ return typeof id === 'number' ? id : -1;
131
+ },
132
+ endBackgroundTask: (id) => module.endBackgroundTask(id),
133
+ };
134
+ }
135
+
136
+ /** Builds the platform adapter for a real React Native app. */
137
+ export function createReactNativePlatform(): PlatformAdapter {
138
+ const os: 'ios' | 'android' = Platform.OS === 'ios' ? 'ios' : 'android';
139
+ const module = resolveNativeModule();
140
+ const native = module ? wrapNative(module, os) : createFallbackBridge(os);
141
+
142
+ let nativeEvents: NativeEventSource;
143
+ if (module) {
144
+ // iOS requires the module instance; on Android it lets the emitter call
145
+ // addListener/removeListeners on the module.
146
+ const emitter = new NativeEventEmitter(module as unknown as ConstructorParameters<typeof NativeEventEmitter>[0]);
147
+ nativeEvents = {
148
+ addListener: (eventName, listener) => emitter.addListener(eventName, listener),
149
+ };
150
+ } else {
151
+ nativeEvents = { addListener: () => ({ remove: () => {} }) };
152
+ }
153
+
154
+ const fetchImpl: FetchLike = (url, init) =>
155
+ fetch(url, init as RequestInit) as unknown as ReturnType<FetchLike>;
156
+
157
+ return {
158
+ os,
159
+ native,
160
+ nativeEvents,
161
+ appState: {
162
+ get currentState() {
163
+ return AppState.currentState;
164
+ },
165
+ addEventListener: (type, listener) => AppState.addEventListener(type, listener),
166
+ },
167
+ fetch: fetchImpl,
168
+ scheduler: defaultScheduler,
169
+ now: () => Date.now(),
170
+ random: () => Math.random(),
171
+ };
172
+ }