@chipmobilesdk/rn-notification 0.4.0 → 0.5.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/CHANGELOG.md +257 -166
- package/README.md +420 -410
- package/package.json +61 -75
- package/skills/sdk-rn-notification/SKILL.md +172 -173
- package/src/channel/migrate.ts +285 -289
- package/src/channel/tone-map.ts +130 -130
- package/src/channel/validate-tone.ts +61 -61
- package/src/contract/channel.ts +192 -188
- package/src/contract/diagnostics.ts +102 -102
- package/src/contract/errors.ts +142 -134
- package/src/contract/index.ts +10 -10
- package/src/contract/interaction.ts +62 -62
- package/src/contract/notification.ts +118 -118
- package/src/contract/permission.ts +55 -60
- package/src/contract/reconcile.ts +88 -88
- package/src/contract/recurrence.ts +129 -129
- package/src/diagnostics/burst.ts +101 -101
- package/src/diagnostics/events.ts +81 -72
- package/src/diagnostics/redact.ts +55 -72
- package/src/diagnostics/snapshot.ts +100 -64
- package/src/engine/adapter.ts +172 -172
- package/src/engine/codec.ts +89 -89
- package/src/engine/notify-kit-engine.ts +715 -707
- package/src/index.ts +208 -208
- package/src/initialize.ts +247 -236
- package/src/interaction/index.ts +146 -146
- package/src/maintenance/cycle.ts +150 -154
- package/src/maintenance/permission-refresh.ts +39 -41
- package/src/maintenance/reanchor.ts +101 -101
- package/src/maintenance/toggle.ts +66 -64
- package/src/maintenance/verify.ts +83 -83
- package/src/maintenance/window-refresh.ts +64 -64
- package/src/permission/index.ts +183 -183
- package/src/permission/state.ts +77 -77
- package/src/push/envelope.ts +165 -165
- package/src/push/fcm-provider.ts +150 -146
- package/src/push/gate.ts +190 -190
- package/src/push/index.ts +33 -29
- package/src/push/instructions.ts +114 -109
- package/src/push/provider-adapter.ts +54 -54
- package/src/push/signout.ts +65 -65
- package/src/reconcile/chunk.ts +53 -53
- package/src/reconcile/diff.ts +142 -145
- package/src/reconcile/duplicates.ts +73 -73
- package/src/reconcile/index.ts +180 -180
- package/src/reconcile/truncate.ts +56 -56
- package/src/recurrence/clock-guard.ts +73 -71
- package/src/recurrence/expand.ts +326 -319
- package/src/recurrence/index.ts +270 -259
- package/src/repeat/expiry.ts +53 -55
- package/src/repeat/index.ts +54 -54
- package/src/repeat/marker.ts +72 -72
- package/src/repeat/place.ts +73 -73
- package/src/schedule/anchor.ts +294 -267
- package/src/schedule/index.ts +174 -181
- package/src/schedule/partition.ts +88 -85
- package/src/schedule/resolve-moment.ts +80 -80
- package/src/schedule/validate-content.ts +111 -111
- package/src/schedule/validate-identifier.ts +126 -126
- package/src/schedule/writer.ts +55 -55
- package/src/state/caps.ts +71 -71
- package/src/state/dedup-ledger.ts +35 -35
- package/src/state/interaction-buffer.ts +101 -73
- package/src/state/mmkv-store.ts +92 -75
- package/src/state/permission-history.ts +41 -41
- package/src/state/record.ts +270 -263
- package/src/state/reset.ts +41 -41
- package/src/state/series-ledger.ts +91 -91
- package/src/state/store.ts +43 -40
- package/src/testing/conformance/index.ts +258 -248
- package/src/testing/fake-engine.ts +462 -468
- package/src/testing/fake-provider.ts +126 -129
- package/src/testing/faults.ts +72 -72
- package/src/testing/index.ts +23 -28
- package/src/testing/measure.ts +337 -337
package/src/push/envelope.ts
CHANGED
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Payload normalization.
|
|
3
|
-
*
|
|
4
|
-
* Every provider's payload is reduced to one envelope shape before it reaches
|
|
5
|
-
* the consumer, so changing provider costs consumer code nothing.
|
|
6
|
-
*
|
|
7
|
-
* Version handling is best-effort: a payload declaring a version this build
|
|
8
|
-
* does not implement is still processed, honouring the fields it understands
|
|
9
|
-
* and ignoring the rest. Servers deploy ahead of app adoption and old app
|
|
10
|
-
* versions live on devices for months, so gating every server-side change on
|
|
11
|
-
* app rollout is the more expensive failure.
|
|
12
|
-
*
|
|
13
|
-
* That is only safe under an append-only discipline — a released field's
|
|
14
|
-
* meaning must never be repurposed. The version-skew diagnostic exists so a
|
|
15
|
-
* misprocessing incident is traceable to the skew rather than investigated as a
|
|
16
|
-
* logic defect.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type { PushEnvelope } from './provider-adapter';
|
|
20
|
-
import { DIAGNOSTIC_CODES, NotificationError } from '../contract/errors';
|
|
21
|
-
|
|
22
|
-
/** The contract version this build implements. */
|
|
23
|
-
export const IMPLEMENTED_PAYLOAD_VERSION = 1;
|
|
24
|
-
|
|
25
|
-
const KINDS = new Set(['display', 'data', 'schedule', 'cancel']);
|
|
26
|
-
|
|
27
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
28
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
29
|
-
? (value as Record<string, unknown>)
|
|
30
|
-
: undefined;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Text, or nothing.
|
|
35
|
-
*
|
|
36
|
-
* Deliberately not `String(value)`: an object coerces to the literal string
|
|
37
|
-
* "[object Object]", which would be rendered to the user as a notification
|
|
38
|
-
* title. A field that is not text is treated as absent.
|
|
39
|
-
*/
|
|
40
|
-
function asText(value: unknown): string | undefined {
|
|
41
|
-
if (typeof value === 'string') return value;
|
|
42
|
-
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** An epoch instant from either a number or an ISO-8601 string. */
|
|
47
|
-
function asInstant(value: unknown): number {
|
|
48
|
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
49
|
-
if (typeof value === 'string') return Date.parse(value);
|
|
50
|
-
return Number.NaN;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Extracts and validates the contract version, which every payload must declare. */
|
|
54
|
-
function readVersion(body: Record<string, unknown>): number {
|
|
55
|
-
const raw = body.v;
|
|
56
|
-
const version = typeof raw === 'number' ? raw : Number.parseInt(asText(raw) ?? '', 10);
|
|
57
|
-
|
|
58
|
-
if (!Number.isInteger(version) || version < 1) {
|
|
59
|
-
throw new NotificationError(
|
|
60
|
-
DIAGNOSTIC_CODES.PAYLOAD_MALFORMED,
|
|
61
|
-
'Push payload carries no usable contract version. Every payload must declare `v` from the first release.',
|
|
62
|
-
{ received: typeof raw },
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
return version;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function readKind(body: Record<string, unknown>): PushEnvelope['kind'] {
|
|
69
|
-
const kind = asText(body.kind);
|
|
70
|
-
|
|
71
|
-
if (kind === undefined || !KINDS.has(kind)) {
|
|
72
|
-
throw new NotificationError(
|
|
73
|
-
DIAGNOSTIC_CODES.PAYLOAD_MALFORMED,
|
|
74
|
-
`Push payload declares unknown kind "${kind ?? ''}".`,
|
|
75
|
-
{ kind },
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
return kind as PushEnvelope['kind'];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function readId(body: Record<string, unknown>): string {
|
|
82
|
-
const id = asText(body.id);
|
|
83
|
-
|
|
84
|
-
if (id === undefined || id.length === 0) {
|
|
85
|
-
throw new NotificationError(
|
|
86
|
-
DIAGNOSTIC_CODES.PAYLOAD_MALFORMED,
|
|
87
|
-
'Push payload carries no identifier.',
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
return id;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function readContent(body: Record<string, unknown>): PushEnvelope['content'] {
|
|
94
|
-
const content = asRecord(body.content);
|
|
95
|
-
if (content === undefined) return undefined;
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
title: asText(content.title) ?? '',
|
|
99
|
-
body: asText(content.body) ?? '',
|
|
100
|
-
subtitle: asText(content.subtitle),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function readMoment(body: Record<string, unknown>): PushEnvelope['moment'] {
|
|
105
|
-
const moment = asRecord(body.moment);
|
|
106
|
-
if (moment === undefined) return undefined;
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
at: asInstant(moment.at),
|
|
110
|
-
anchoring: moment.anchoring === 'absolute' ? 'absolute' : 'wallClock',
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface NormalizeResult {
|
|
115
|
-
readonly envelope: PushEnvelope;
|
|
116
|
-
readonly versionSkew: boolean;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Normalizes a raw provider payload.
|
|
121
|
-
*
|
|
122
|
-
* Throws only for a **structurally unparseable** payload — a broken message.
|
|
123
|
-
* An unrecognized version is a healthy message from a newer contract and must
|
|
124
|
-
* never collapse into the same condition.
|
|
125
|
-
*/
|
|
126
|
-
export function normalizeEnvelope(raw: unknown, receivedAt: number): NormalizeResult {
|
|
127
|
-
const record = asRecord(raw);
|
|
128
|
-
if (record === undefined) {
|
|
129
|
-
throw new NotificationError(
|
|
130
|
-
DIAGNOSTIC_CODES.PAYLOAD_MALFORMED,
|
|
131
|
-
'Push payload is not an object and cannot be interpreted.',
|
|
132
|
-
{ received: Array.isArray(raw) ? 'array' : typeof raw },
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Providers deliver the payload under a data bag; accept either shape so the
|
|
137
|
-
// adapter does not have to reshape before handing it over.
|
|
138
|
-
const body = asRecord(record.data) ?? record;
|
|
139
|
-
|
|
140
|
-
const version = readVersion(body);
|
|
141
|
-
const kind = readKind(body);
|
|
142
|
-
const id = readId(body);
|
|
143
|
-
const versionSkew = version !== IMPLEMENTED_PAYLOAD_VERSION;
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
versionSkew,
|
|
147
|
-
envelope: {
|
|
148
|
-
version,
|
|
149
|
-
kind,
|
|
150
|
-
id,
|
|
151
|
-
ownerTag: asText(body.ownerTag),
|
|
152
|
-
content: readContent(body),
|
|
153
|
-
tone: asText(body.tone),
|
|
154
|
-
moment: readMoment(body),
|
|
155
|
-
routing: asRecord(body.routing),
|
|
156
|
-
// Falls back to the identifier, so a server that omits it still gets
|
|
157
|
-
// de-duplication rather than silently losing it.
|
|
158
|
-
dedupId: asText(body.dedupId) ?? id,
|
|
159
|
-
receivedAt,
|
|
160
|
-
versionSkew,
|
|
161
|
-
// Fields this build does not understand are simply not read — that is
|
|
162
|
-
// what best-effort processing means in practice.
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Payload normalization.
|
|
3
|
+
*
|
|
4
|
+
* Every provider's payload is reduced to one envelope shape before it reaches
|
|
5
|
+
* the consumer, so changing provider costs consumer code nothing.
|
|
6
|
+
*
|
|
7
|
+
* Version handling is best-effort: a payload declaring a version this build
|
|
8
|
+
* does not implement is still processed, honouring the fields it understands
|
|
9
|
+
* and ignoring the rest. Servers deploy ahead of app adoption and old app
|
|
10
|
+
* versions live on devices for months, so gating every server-side change on
|
|
11
|
+
* app rollout is the more expensive failure.
|
|
12
|
+
*
|
|
13
|
+
* That is only safe under an append-only discipline — a released field's
|
|
14
|
+
* meaning must never be repurposed. The version-skew diagnostic exists so a
|
|
15
|
+
* misprocessing incident is traceable to the skew rather than investigated as a
|
|
16
|
+
* logic defect.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { PushEnvelope } from './provider-adapter';
|
|
20
|
+
import { DIAGNOSTIC_CODES, NotificationError } from '../contract/errors';
|
|
21
|
+
|
|
22
|
+
/** The contract version this build implements. */
|
|
23
|
+
export const IMPLEMENTED_PAYLOAD_VERSION = 1;
|
|
24
|
+
|
|
25
|
+
const KINDS = new Set(['display', 'data', 'schedule', 'cancel']);
|
|
26
|
+
|
|
27
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
28
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
29
|
+
? (value as Record<string, unknown>)
|
|
30
|
+
: undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Text, or nothing.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately not `String(value)`: an object coerces to the literal string
|
|
37
|
+
* "[object Object]", which would be rendered to the user as a notification
|
|
38
|
+
* title. A field that is not text is treated as absent.
|
|
39
|
+
*/
|
|
40
|
+
function asText(value: unknown): string | undefined {
|
|
41
|
+
if (typeof value === 'string') return value;
|
|
42
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** An epoch instant from either a number or an ISO-8601 string. */
|
|
47
|
+
function asInstant(value: unknown): number {
|
|
48
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
49
|
+
if (typeof value === 'string') return Date.parse(value);
|
|
50
|
+
return Number.NaN;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Extracts and validates the contract version, which every payload must declare. */
|
|
54
|
+
function readVersion(body: Record<string, unknown>): number {
|
|
55
|
+
const raw = body.v;
|
|
56
|
+
const version = typeof raw === 'number' ? raw : Number.parseInt(asText(raw) ?? '', 10);
|
|
57
|
+
|
|
58
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
59
|
+
throw new NotificationError(
|
|
60
|
+
DIAGNOSTIC_CODES.PAYLOAD_MALFORMED,
|
|
61
|
+
'Push payload carries no usable contract version. Every payload must declare `v` from the first release.',
|
|
62
|
+
{ received: typeof raw },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return version;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readKind(body: Record<string, unknown>): PushEnvelope['kind'] {
|
|
69
|
+
const kind = asText(body.kind);
|
|
70
|
+
|
|
71
|
+
if (kind === undefined || !KINDS.has(kind)) {
|
|
72
|
+
throw new NotificationError(
|
|
73
|
+
DIAGNOSTIC_CODES.PAYLOAD_MALFORMED,
|
|
74
|
+
`Push payload declares unknown kind "${kind ?? ''}".`,
|
|
75
|
+
{ kind },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return kind as PushEnvelope['kind'];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readId(body: Record<string, unknown>): string {
|
|
82
|
+
const id = asText(body.id);
|
|
83
|
+
|
|
84
|
+
if (id === undefined || id.length === 0) {
|
|
85
|
+
throw new NotificationError(
|
|
86
|
+
DIAGNOSTIC_CODES.PAYLOAD_MALFORMED,
|
|
87
|
+
'Push payload carries no identifier.',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return id;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readContent(body: Record<string, unknown>): PushEnvelope['content'] {
|
|
94
|
+
const content = asRecord(body.content);
|
|
95
|
+
if (content === undefined) return undefined;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
title: asText(content.title) ?? '',
|
|
99
|
+
body: asText(content.body) ?? '',
|
|
100
|
+
subtitle: asText(content.subtitle),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readMoment(body: Record<string, unknown>): PushEnvelope['moment'] {
|
|
105
|
+
const moment = asRecord(body.moment);
|
|
106
|
+
if (moment === undefined) return undefined;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
at: asInstant(moment.at),
|
|
110
|
+
anchoring: moment.anchoring === 'absolute' ? 'absolute' : 'wallClock',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface NormalizeResult {
|
|
115
|
+
readonly envelope: PushEnvelope;
|
|
116
|
+
readonly versionSkew: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Normalizes a raw provider payload.
|
|
121
|
+
*
|
|
122
|
+
* Throws only for a **structurally unparseable** payload — a broken message.
|
|
123
|
+
* An unrecognized version is a healthy message from a newer contract and must
|
|
124
|
+
* never collapse into the same condition.
|
|
125
|
+
*/
|
|
126
|
+
export function normalizeEnvelope(raw: unknown, receivedAt: number): NormalizeResult {
|
|
127
|
+
const record = asRecord(raw);
|
|
128
|
+
if (record === undefined) {
|
|
129
|
+
throw new NotificationError(
|
|
130
|
+
DIAGNOSTIC_CODES.PAYLOAD_MALFORMED,
|
|
131
|
+
'Push payload is not an object and cannot be interpreted.',
|
|
132
|
+
{ received: Array.isArray(raw) ? 'array' : typeof raw },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Providers deliver the payload under a data bag; accept either shape so the
|
|
137
|
+
// adapter does not have to reshape before handing it over.
|
|
138
|
+
const body = asRecord(record.data) ?? record;
|
|
139
|
+
|
|
140
|
+
const version = readVersion(body);
|
|
141
|
+
const kind = readKind(body);
|
|
142
|
+
const id = readId(body);
|
|
143
|
+
const versionSkew = version !== IMPLEMENTED_PAYLOAD_VERSION;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
versionSkew,
|
|
147
|
+
envelope: {
|
|
148
|
+
version,
|
|
149
|
+
kind,
|
|
150
|
+
id,
|
|
151
|
+
ownerTag: asText(body.ownerTag),
|
|
152
|
+
content: readContent(body),
|
|
153
|
+
tone: asText(body.tone),
|
|
154
|
+
moment: readMoment(body),
|
|
155
|
+
routing: asRecord(body.routing),
|
|
156
|
+
// Falls back to the identifier, so a server that omits it still gets
|
|
157
|
+
// de-duplication rather than silently losing it.
|
|
158
|
+
dedupId: asText(body.dedupId) ?? id,
|
|
159
|
+
receivedAt,
|
|
160
|
+
versionSkew,
|
|
161
|
+
// Fields this build does not understand are simply not read — that is
|
|
162
|
+
// what best-effort processing means in practice.
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
package/src/push/fcm-provider.ts
CHANGED
|
@@ -1,146 +1,150 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Firebase Cloud Messaging provider.
|
|
3
|
-
*
|
|
4
|
-
* Every `@react-native-firebase/messaging` import is confined to this file, so
|
|
5
|
-
* the rest of the package — and every consuming app — stays free of provider
|
|
6
|
-
* types. Substituting a provider is an adapter swap, not a rewrite.
|
|
7
|
-
*
|
|
8
|
-
* The module is required **lazily**. That matters more here than for the
|
|
9
|
-
* engine: messaging is an *optional* peer dependency, and an app that never
|
|
10
|
-
* enables push must not fail at import time for a package it deliberately did
|
|
11
|
-
* not install.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from '
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return '
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The Firebase Cloud Messaging provider.
|
|
3
|
+
*
|
|
4
|
+
* Every `@react-native-firebase/messaging` import is confined to this file, so
|
|
5
|
+
* the rest of the package — and every consuming app — stays free of provider
|
|
6
|
+
* types. Substituting a provider is an adapter swap, not a rewrite.
|
|
7
|
+
*
|
|
8
|
+
* The module is required **lazily**. That matters more here than for the
|
|
9
|
+
* engine: messaging is an *optional* peer dependency, and an app that never
|
|
10
|
+
* enables push must not fail at import time for a package it deliberately did
|
|
11
|
+
* not install.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { lazyPeer } from '@chipmobilesdk/rn-core';
|
|
15
|
+
|
|
16
|
+
import type { PushAvailability, PushProviderAdapter } from './provider-adapter';
|
|
17
|
+
import type { Unsubscribe } from '../contract/interaction';
|
|
18
|
+
|
|
19
|
+
/** Minimal shape used from the SDK, so its full surface never leaks into typing. */
|
|
20
|
+
interface MessagingModule {
|
|
21
|
+
(): MessagingInstance;
|
|
22
|
+
AuthorizationStatus?: Record<string, number>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface MessagingInstance {
|
|
26
|
+
getToken(): Promise<string>;
|
|
27
|
+
deleteToken(): Promise<void>;
|
|
28
|
+
onTokenRefresh(handler: (token: string) => void): () => void;
|
|
29
|
+
onMessage(handler: (message: unknown) => Promise<void> | void): () => void;
|
|
30
|
+
setBackgroundMessageHandler(handler: (message: unknown) => Promise<void>): void;
|
|
31
|
+
getInitialNotification(): Promise<unknown | null>;
|
|
32
|
+
hasPermission?(): Promise<number>;
|
|
33
|
+
isDeviceRegisteredForRemoteMessages?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const messagingPeer = lazyPeer('@react-native-firebase/messaging', () => {
|
|
37
|
+
// Inside `try`, so Metro bundles an app that leaves this optional peer out.
|
|
38
|
+
try {
|
|
39
|
+
return require('@react-native-firebase/messaging') as { default: MessagingModule };
|
|
40
|
+
} catch (cause) {
|
|
41
|
+
throw cause;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function loadMessaging(): MessagingModule | undefined {
|
|
46
|
+
// Absent optional peer, or missing native module: reported as unavailability
|
|
47
|
+
// through the normal path rather than thrown at import.
|
|
48
|
+
const result = messagingPeer.get();
|
|
49
|
+
return result.ok ? result.module.default : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FcmProviderOptions {
|
|
53
|
+
/** Injected in tests; production resolves the real module. */
|
|
54
|
+
readonly module?: MessagingModule;
|
|
55
|
+
/**
|
|
56
|
+
* Registers the background message handler.
|
|
57
|
+
*
|
|
58
|
+
* Left to the consumer by default because the SDK requires it to be
|
|
59
|
+
* registered at module scope in `index.js`, outside React's lifecycle — a
|
|
60
|
+
* constraint the package documents rather than works around.
|
|
61
|
+
*/
|
|
62
|
+
readonly onBackgroundMessage?: (message: unknown) => Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createFcmProvider(options: FcmProviderOptions = {}): PushProviderAdapter {
|
|
66
|
+
let cached: MessagingModule | undefined = options.module;
|
|
67
|
+
let resolved = options.module !== undefined;
|
|
68
|
+
|
|
69
|
+
const messaging = (): MessagingInstance | undefined => {
|
|
70
|
+
if (!resolved) {
|
|
71
|
+
cached = loadMessaging();
|
|
72
|
+
resolved = true;
|
|
73
|
+
}
|
|
74
|
+
return cached?.();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (options.onBackgroundMessage !== undefined) {
|
|
78
|
+
messaging()?.setBackgroundMessageHandler(options.onBackgroundMessage);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
name: '@react-native-firebase/messaging',
|
|
83
|
+
|
|
84
|
+
async getAvailability(): Promise<PushAvailability> {
|
|
85
|
+
const instance = messaging();
|
|
86
|
+
|
|
87
|
+
// A device without Google Play Services, or an app that never installed
|
|
88
|
+
// the optional peer. Either way the local path is unaffected.
|
|
89
|
+
if (instance === undefined) return 'unavailable';
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await instance.getToken();
|
|
93
|
+
return 'available';
|
|
94
|
+
} catch {
|
|
95
|
+
// Registration can fail for reasons the SDK does not distinguish, so
|
|
96
|
+
// `unknown` is reported rather than a guess at which one.
|
|
97
|
+
return 'unknown';
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async getToken(): Promise<string> {
|
|
102
|
+
const instance = messaging();
|
|
103
|
+
if (instance === undefined) {
|
|
104
|
+
throw new Error('Push provider is unavailable on this device.');
|
|
105
|
+
}
|
|
106
|
+
// Resolves only once a genuine token exists; the gate never presents an
|
|
107
|
+
// empty value as if it were one.
|
|
108
|
+
return instance.getToken();
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
onTokenChange(handler: (token: string) => void): Unsubscribe {
|
|
112
|
+
const instance = messaging();
|
|
113
|
+
if (instance === undefined) return () => undefined;
|
|
114
|
+
return instance.onTokenRefresh(handler);
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async invalidateToken(): Promise<boolean> {
|
|
118
|
+
const instance = messaging();
|
|
119
|
+
if (instance === undefined) return false;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await instance.deleteToken();
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
// Reported as a failed invalidation rather than assumed successful —
|
|
126
|
+
// the device may keep receiving the previous account's messages.
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
subscribeToMessages(handler: (raw: unknown) => void): Unsubscribe {
|
|
132
|
+
const instance = messaging();
|
|
133
|
+
if (instance === undefined) return () => undefined;
|
|
134
|
+
|
|
135
|
+
// Foreground only. Background delivery arrives through the handler the
|
|
136
|
+
// consumer registers at module scope, which the README documents.
|
|
137
|
+
return instance.onMessage(message => {
|
|
138
|
+
handler(message);
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
async takeInitialMessage(): Promise<unknown | undefined> {
|
|
143
|
+
const instance = messaging();
|
|
144
|
+
if (instance === undefined) return undefined;
|
|
145
|
+
|
|
146
|
+
const message = await instance.getInitialNotification();
|
|
147
|
+
return message ?? undefined;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|