@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
|
@@ -1,72 +1,55 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content redaction for diagnostics.
|
|
3
|
-
*
|
|
4
|
-
* A notification library that logs notification bodies has turned a diagnostic
|
|
5
|
-
* aid into a privacy incident. Nothing content-bearing reaches an event unless
|
|
6
|
-
* verbose diagnostics is explicitly enabled — and that setting is documented as
|
|
7
|
-
* unsuitable for production.
|
|
8
|
-
*
|
|
9
|
-
* Values are replaced by their *shape* — a length, a type, a character position
|
|
10
|
-
* — which is what actually helps someone debug a validation failure anyway.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (depth > 6 || value === null || typeof value !== 'object') return value;
|
|
57
|
-
if (Array.isArray(value)) return value.map(item => walk(item, depth + 1));
|
|
58
|
-
|
|
59
|
-
const output: Record<string, unknown> = {};
|
|
60
|
-
for (const [key, inner] of Object.entries(value as Record<string, unknown>)) {
|
|
61
|
-
output[key] = REDACTED_KEYS.has(key) ? describe(inner) : walk(inner, depth + 1);
|
|
62
|
-
}
|
|
63
|
-
return output;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
return walk(context, 0) as Readonly<Record<string, unknown>>;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** True when a key would be redacted. Exported so tests can assert the policy directly. */
|
|
70
|
-
export function isRedactedKey(key: string): boolean {
|
|
71
|
-
return REDACTED_KEYS.has(key);
|
|
72
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Content redaction for diagnostics.
|
|
3
|
+
*
|
|
4
|
+
* A notification library that logs notification bodies has turned a diagnostic
|
|
5
|
+
* aid into a privacy incident. Nothing content-bearing reaches an event unless
|
|
6
|
+
* verbose diagnostics is explicitly enabled — and that setting is documented as
|
|
7
|
+
* unsuitable for production.
|
|
8
|
+
*
|
|
9
|
+
* Values are replaced by their *shape* — a length, a type, a character position
|
|
10
|
+
* — which is what actually helps someone debug a validation failure anyway.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createRedactor } from '@chipmobilesdk/rn-core';
|
|
14
|
+
|
|
15
|
+
/** Keys whose values are never emitted at any verbosity below verbose. */
|
|
16
|
+
const REDACTED_KEYS = new Set([
|
|
17
|
+
'title',
|
|
18
|
+
'body',
|
|
19
|
+
'subtitle',
|
|
20
|
+
'content',
|
|
21
|
+
'routing',
|
|
22
|
+
'routingPayload',
|
|
23
|
+
'payload',
|
|
24
|
+
'token',
|
|
25
|
+
'pushToken',
|
|
26
|
+
'inputText',
|
|
27
|
+
'text',
|
|
28
|
+
'data',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* rn-core's redactor: the keys above are replaced by their shape (a length, a
|
|
33
|
+
* type), and every other string is pattern-redacted, so a token or an e-mail
|
|
34
|
+
* address under an innocent key such as `reason` does not get through either.
|
|
35
|
+
*/
|
|
36
|
+
const redactor = createRedactor({ shapeKeys: REDACTED_KEYS });
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Strips content values from a diagnostic context.
|
|
40
|
+
*
|
|
41
|
+
* Recurses so a nested `{ request: { content: { body } } }` is caught — a
|
|
42
|
+
* top-level-only scrub is the kind of redaction that leaks the first time
|
|
43
|
+
* anyone nests a payload.
|
|
44
|
+
*/
|
|
45
|
+
export function redactContext(
|
|
46
|
+
context: Readonly<Record<string, unknown>> | undefined,
|
|
47
|
+
verbose: boolean,
|
|
48
|
+
): Readonly<Record<string, unknown>> | undefined {
|
|
49
|
+
return verbose ? context : redactor.context(context);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** True when a key would be redacted. Exported so tests can assert the policy directly. */
|
|
53
|
+
export function isRedactedKey(key: string): boolean {
|
|
54
|
+
return REDACTED_KEYS.has(key);
|
|
55
|
+
}
|
|
@@ -1,64 +1,100 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The state snapshot.
|
|
3
|
-
*
|
|
4
|
-
* The readable answer to "why didn't it arrive?" — assembled in one place so
|
|
5
|
-
* that question can be answered without attaching a native debugger.
|
|
6
|
-
*
|
|
7
|
-
* The push token appears as **presence only**. Its value is never exposed here
|
|
8
|
-
* or anywhere else, because a snapshot is the thing most likely to be pasted
|
|
9
|
-
* into a bug report.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
import type {
|
|
15
|
-
import type {
|
|
16
|
-
import type {
|
|
17
|
-
import type {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
readonly
|
|
23
|
-
readonly
|
|
24
|
-
readonly
|
|
25
|
-
readonly
|
|
26
|
-
readonly
|
|
27
|
-
readonly
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The state snapshot.
|
|
3
|
+
*
|
|
4
|
+
* The readable answer to "why didn't it arrive?" — assembled in one place so
|
|
5
|
+
* that question can be answered without attaching a native debugger.
|
|
6
|
+
*
|
|
7
|
+
* The push token appears as **presence only**. Its value is never exposed here
|
|
8
|
+
* or anywhere else, because a snapshot is the thing most likely to be pasted
|
|
9
|
+
* into a bug report.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describeShape } from '@chipmobilesdk/rn-core';
|
|
13
|
+
|
|
14
|
+
import type { StateSnapshot } from '../contract/diagnostics';
|
|
15
|
+
import type { PendingEntry } from '../contract/notification';
|
|
16
|
+
import type { PermissionReport, RestrictionReport } from '../contract/permission';
|
|
17
|
+
import type { MaintenanceResult, PendingDiscrepancy, ReconcileResult } from '../contract/reconcile';
|
|
18
|
+
import type { NotificationEngineAdapter } from '../engine/adapter';
|
|
19
|
+
import type { StateManager } from '../state/record';
|
|
20
|
+
|
|
21
|
+
export interface SnapshotDependencies {
|
|
22
|
+
readonly engine: NotificationEngineAdapter;
|
|
23
|
+
readonly state: StateManager;
|
|
24
|
+
readonly now: () => number;
|
|
25
|
+
readonly permissions: () => Promise<PermissionReport>;
|
|
26
|
+
readonly restrictions: () => Promise<RestrictionReport>;
|
|
27
|
+
readonly appliedTones: () => readonly {
|
|
28
|
+
readonly toneId: string;
|
|
29
|
+
readonly appliedVersion: number;
|
|
30
|
+
}[];
|
|
31
|
+
readonly lastReconcile: () => ReconcileResult | undefined;
|
|
32
|
+
readonly lastMaintenance: () => MaintenanceResult | undefined;
|
|
33
|
+
/** Presence only — never the value. */
|
|
34
|
+
readonly hasPushToken: () => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Whether verbose diagnostics are on. Off (the default), pending entries carry
|
|
37
|
+
* the shape of their content and routing (`[string:12]`), not the values.
|
|
38
|
+
*/
|
|
39
|
+
readonly verbose?: () => boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A pending entry with content replaced by its shape. A snapshot is the thing
|
|
44
|
+
* most likely to be pasted into a bug report, so it never carries notification
|
|
45
|
+
* text or routing values unless verbose diagnostics is on (NOT-14).
|
|
46
|
+
*/
|
|
47
|
+
function withoutContent(entry: PendingEntry): PendingEntry {
|
|
48
|
+
const { content, routing } = entry;
|
|
49
|
+
return {
|
|
50
|
+
...entry,
|
|
51
|
+
content: {
|
|
52
|
+
title: describeShape(content.title),
|
|
53
|
+
body: describeShape(content.body),
|
|
54
|
+
...(content.subtitle === undefined ? {} : { subtitle: describeShape(content.subtitle) }),
|
|
55
|
+
},
|
|
56
|
+
...(routing === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: {
|
|
59
|
+
routing: Object.fromEntries(
|
|
60
|
+
Object.entries(routing).map(([key, value]) => [key, describeShape(value)]),
|
|
61
|
+
),
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createSnapshot(deps: SnapshotDependencies) {
|
|
67
|
+
return {
|
|
68
|
+
async capture(): Promise<StateSnapshot> {
|
|
69
|
+
const [permissions, restrictions, pending] = await Promise.all([
|
|
70
|
+
deps.permissions(),
|
|
71
|
+
deps.restrictions(),
|
|
72
|
+
deps.engine.getPending(),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const maintenance = deps.lastMaintenance();
|
|
76
|
+
const discrepancy: PendingDiscrepancy | undefined = maintenance?.discrepancy;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
permissions,
|
|
80
|
+
restrictions,
|
|
81
|
+
tones: deps.appliedTones().map(tone => ({
|
|
82
|
+
id: tone.toneId,
|
|
83
|
+
appliedVersion: tone.appliedVersion,
|
|
84
|
+
})),
|
|
85
|
+
pendingCount: pending.length,
|
|
86
|
+
pending: deps.verbose?.()
|
|
87
|
+
? (pending as readonly PendingEntry[])
|
|
88
|
+
: pending.map(entry => withoutContent(entry as PendingEntry)),
|
|
89
|
+
lastReconcile: deps.lastReconcile(),
|
|
90
|
+
lastMaintenance: maintenance,
|
|
91
|
+
discrepancy,
|
|
92
|
+
stateHealth: deps.state.health(),
|
|
93
|
+
pushTokenPresent: deps.hasPushToken(),
|
|
94
|
+
capturedAt: deps.now(),
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type Snapshot = ReturnType<typeof createSnapshot>;
|
package/src/engine/adapter.ts
CHANGED
|
@@ -1,172 +1,172 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The port every local notification engine satisfies.
|
|
3
|
-
*
|
|
4
|
-
* No engine type appears in any signature here. That is what makes the engine
|
|
5
|
-
* replaceable — and it has already been exercised once, when the archived
|
|
6
|
-
* Notifee was swapped for its maintained fork with no consumer impact.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { PendingEntry, NotificationRequest } from '../contract/notification';
|
|
10
|
-
import type { PermissionKind, PermissionState, RestrictionReport } from '../contract/permission';
|
|
11
|
-
import type { ResolvedTone, ChannelGroup } from '../contract/channel';
|
|
12
|
-
import type { InteractionEvent, Unsubscribe } from '../contract/interaction';
|
|
13
|
-
|
|
14
|
-
/** A request whose anchoring has already been resolved to a firing instant. */
|
|
15
|
-
export interface EngineScheduleRequest {
|
|
16
|
-
readonly request: NotificationRequest;
|
|
17
|
-
/** The absolute instant the platform should fire, after anchoring resolution. */
|
|
18
|
-
readonly resolvedAt: number;
|
|
19
|
-
/** Resolved platform channel and category for the request's tone. */
|
|
20
|
-
readonly tone: ResolvedTone;
|
|
21
|
-
/**
|
|
22
|
-
* The IANA time zone this moment was anchored against.
|
|
23
|
-
*
|
|
24
|
-
* Carried explicitly because the adapter has to round-trip it through the
|
|
25
|
-
* platform data bag — `getPending` must report it (FR-010) so a consumer can
|
|
26
|
-
* detect the one case re-anchoring cannot fix (FR-007c). Absent for
|
|
27
|
-
* absolute-instant moments, which are never anchored to a zone at all.
|
|
28
|
-
*/
|
|
29
|
-
readonly anchoringGeneration?: string;
|
|
30
|
-
/** True when exact-alarm capability is unavailable and approximate mode applies. */
|
|
31
|
-
readonly approximate: boolean;
|
|
32
|
-
readonly seriesId?: string;
|
|
33
|
-
readonly pushCreated?: boolean;
|
|
34
|
-
/**
|
|
35
|
-
* Marks this request as a package-owned stop marker rather than a consumer
|
|
36
|
-
* notification.
|
|
37
|
-
*
|
|
38
|
-
* Carried here rather than smuggled through `ownerTag` or `routing` so it can
|
|
39
|
-
* never collide with a value a consumer owns, and so the adapter can keep
|
|
40
|
-
* markers out of `getPending` without inspecting consumer data.
|
|
41
|
-
*/
|
|
42
|
-
readonly repeatStop?: RepeatStop;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface EngineCapabilities {
|
|
46
|
-
/** iOS holds only the 64 soonest pending notifications and silently drops the rest. */
|
|
47
|
-
readonly pendingCeiling: number;
|
|
48
|
-
readonly supportsExactAlarms: boolean;
|
|
49
|
-
readonly supportsChannels: boolean;
|
|
50
|
-
readonly supportsTextInputActions: boolean;
|
|
51
|
-
readonly supportsDoNotDisturbBypass: boolean;
|
|
52
|
-
/**
|
|
53
|
-
* Whether a notification's alert sound can be made to repeat until handled.
|
|
54
|
-
*
|
|
55
|
-
* Decides two things: whether a refused repeat is reported to the caller, and
|
|
56
|
-
* whether a stop marker is placed at all. Where this is false no marker is
|
|
57
|
-
* created, so the platform's pending ceiling is untouched by the feature.
|
|
58
|
-
*/
|
|
59
|
-
readonly supportsRepeatingAlert: boolean;
|
|
60
|
-
/** Milliseconds a background action or silent payload may run for. */
|
|
61
|
-
readonly backgroundWindowMs: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* What applying a tone declaration actually did.
|
|
66
|
-
*
|
|
67
|
-
* A boolean cannot carry this: a caller has to tell a label correction from a
|
|
68
|
-
* newly created channel from a no-op, and reporting a refresh as a creation
|
|
69
|
-
* would make every apply look like a write.
|
|
70
|
-
*/
|
|
71
|
-
export type ToneDeclarationOutcome = 'created' | 'labelsRefreshed' | 'unchanged';
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* A package-owned artifact scheduled to end a repeating alert.
|
|
75
|
-
*
|
|
76
|
-
* Never a consumer notification. It exists because no platform offers "stop the
|
|
77
|
-
* sound at time T but keep the notification", so something has to run at T.
|
|
78
|
-
*/
|
|
79
|
-
export interface RepeatStop {
|
|
80
|
-
readonly markerId: string;
|
|
81
|
-
/** The consumer notification whose alert this stops. */
|
|
82
|
-
readonly targetId: string;
|
|
83
|
-
readonly dueAt: number;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** A notification arriving, as opposed to a user acting on one. */
|
|
87
|
-
export interface DeliveryEvent {
|
|
88
|
-
readonly notificationId: string;
|
|
89
|
-
readonly occurredAt: number;
|
|
90
|
-
/** Present when this delivery is a package-owned stop marker rather than a real notification. */
|
|
91
|
-
readonly repeatStop?: RepeatStop;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export interface NotificationEngineAdapter {
|
|
95
|
-
readonly name: string;
|
|
96
|
-
|
|
97
|
-
getCapabilities(): Promise<EngineCapabilities>;
|
|
98
|
-
|
|
99
|
-
/** Scheduling an existing identifier replaces it; exactly one entry results. */
|
|
100
|
-
schedule(request: EngineScheduleRequest): Promise<void>;
|
|
101
|
-
|
|
102
|
-
cancel(id: string): Promise<void>;
|
|
103
|
-
cancelMany(ids: readonly string[]): Promise<void>;
|
|
104
|
-
cancelAll(): Promise<void>;
|
|
105
|
-
|
|
106
|
-
/** Full detail, so callers can diff rather than merely count. */
|
|
107
|
-
getPending(): Promise<readonly PendingEntry[]>;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Applies a channel or category declaration.
|
|
111
|
-
*
|
|
112
|
-
* Must return `labelsRefreshed` only when a label actually changed. Returning
|
|
113
|
-
* it unconditionally would turn every apply into a write and quietly break
|
|
114
|
-
* the zero-operation guarantee reconciliation idempotence rests on.
|
|
115
|
-
*/
|
|
116
|
-
declareTone(tone: ResolvedTone): Promise<ToneDeclarationOutcome>;
|
|
117
|
-
/** Removes a retired channel. Returns true when one was actually removed. */
|
|
118
|
-
retireTone(channelId: string): Promise<boolean>;
|
|
119
|
-
declareGroups(groups: readonly ChannelGroup[]): Promise<void>;
|
|
120
|
-
/** Channel ids the platform currently holds, which re-declaration cannot reveal. */
|
|
121
|
-
listDeclaredChannels(): Promise<readonly string[]>;
|
|
122
|
-
|
|
123
|
-
getPermission(kind: PermissionKind): Promise<PermissionState>;
|
|
124
|
-
requestPermission(kind: PermissionKind): Promise<PermissionState>;
|
|
125
|
-
openSettings(kind: PermissionKind): Promise<void>;
|
|
126
|
-
getRestrictions(): Promise<RestrictionReport>;
|
|
127
|
-
|
|
128
|
-
/** Interactions arriving before a handler registers are the caller's problem to buffer, not the engine's. */
|
|
129
|
-
subscribeToInteractions(handler: (event: InteractionEvent) => void): Unsubscribe;
|
|
130
|
-
/** Any interaction that launched the app, consumed exactly once. */
|
|
131
|
-
takeInitialInteraction(): Promise<InteractionEvent | undefined>;
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Stops a delivered notification's repeating alert without removing it.
|
|
135
|
-
*
|
|
136
|
-
* Re-posts the notification under the same identifier with the alert
|
|
137
|
-
* suppressed. The same identifier means the same tray slot, so it replaces in
|
|
138
|
-
* place: the sound stops and the notification stays. Cancelling instead would
|
|
139
|
-
* be worse than never alerting at all — the user comes home to a silent phone
|
|
140
|
-
* and no idea what they missed.
|
|
141
|
-
*
|
|
142
|
-
* Returns false when the notification is no longer displayed, which is the
|
|
143
|
-
* ordinary case once the user has handled it.
|
|
144
|
-
*/
|
|
145
|
-
silenceDelivered(id: string): Promise<boolean>;
|
|
146
|
-
|
|
147
|
-
/** Package-owned stop markers currently scheduled. Never consumer notifications. */
|
|
148
|
-
listRepeatStops(): Promise<readonly RepeatStop[]>;
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Deliveries, as distinct from interactions.
|
|
152
|
-
*
|
|
153
|
-
* `subscribeToInteractions` deliberately discards delivery events, because
|
|
154
|
-
* reporting an arrival as a tap would cancel a notification the user never
|
|
155
|
-
* touched. Repeat expiry needs exactly the events that filter throws away, so
|
|
156
|
-
* it gets its own subscription rather than loosening that one.
|
|
157
|
-
*/
|
|
158
|
-
subscribeToDeliveries(handler: (event: DeliveryEvent) => void): Unsubscribe;
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Registers the handler that runs when the app process is not alive.
|
|
162
|
-
*
|
|
163
|
-
* Must be called at module scope, outside the React tree, because that is
|
|
164
|
-
* where the engine registers its headless task. The package cannot do this on
|
|
165
|
-
* a consumer's behalf, which is why it is a documented setup step rather than
|
|
166
|
-
* an internal detail — and why its absence is reported rather than guessed at.
|
|
167
|
-
*/
|
|
168
|
-
registerBackgroundDeliveryHandler(handler: (event: DeliveryEvent) => Promise<void>): void;
|
|
169
|
-
|
|
170
|
-
/** Whether a background delivery handler has been registered in this process. */
|
|
171
|
-
hasBackgroundDeliveryHandler(): boolean;
|
|
172
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The port every local notification engine satisfies.
|
|
3
|
+
*
|
|
4
|
+
* No engine type appears in any signature here. That is what makes the engine
|
|
5
|
+
* replaceable — and it has already been exercised once, when the archived
|
|
6
|
+
* Notifee was swapped for its maintained fork with no consumer impact.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PendingEntry, NotificationRequest } from '../contract/notification';
|
|
10
|
+
import type { PermissionKind, PermissionState, RestrictionReport } from '../contract/permission';
|
|
11
|
+
import type { ResolvedTone, ChannelGroup } from '../contract/channel';
|
|
12
|
+
import type { InteractionEvent, Unsubscribe } from '../contract/interaction';
|
|
13
|
+
|
|
14
|
+
/** A request whose anchoring has already been resolved to a firing instant. */
|
|
15
|
+
export interface EngineScheduleRequest {
|
|
16
|
+
readonly request: NotificationRequest;
|
|
17
|
+
/** The absolute instant the platform should fire, after anchoring resolution. */
|
|
18
|
+
readonly resolvedAt: number;
|
|
19
|
+
/** Resolved platform channel and category for the request's tone. */
|
|
20
|
+
readonly tone: ResolvedTone;
|
|
21
|
+
/**
|
|
22
|
+
* The IANA time zone this moment was anchored against.
|
|
23
|
+
*
|
|
24
|
+
* Carried explicitly because the adapter has to round-trip it through the
|
|
25
|
+
* platform data bag — `getPending` must report it (FR-010) so a consumer can
|
|
26
|
+
* detect the one case re-anchoring cannot fix (FR-007c). Absent for
|
|
27
|
+
* absolute-instant moments, which are never anchored to a zone at all.
|
|
28
|
+
*/
|
|
29
|
+
readonly anchoringGeneration?: string;
|
|
30
|
+
/** True when exact-alarm capability is unavailable and approximate mode applies. */
|
|
31
|
+
readonly approximate: boolean;
|
|
32
|
+
readonly seriesId?: string;
|
|
33
|
+
readonly pushCreated?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Marks this request as a package-owned stop marker rather than a consumer
|
|
36
|
+
* notification.
|
|
37
|
+
*
|
|
38
|
+
* Carried here rather than smuggled through `ownerTag` or `routing` so it can
|
|
39
|
+
* never collide with a value a consumer owns, and so the adapter can keep
|
|
40
|
+
* markers out of `getPending` without inspecting consumer data.
|
|
41
|
+
*/
|
|
42
|
+
readonly repeatStop?: RepeatStop;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface EngineCapabilities {
|
|
46
|
+
/** iOS holds only the 64 soonest pending notifications and silently drops the rest. */
|
|
47
|
+
readonly pendingCeiling: number;
|
|
48
|
+
readonly supportsExactAlarms: boolean;
|
|
49
|
+
readonly supportsChannels: boolean;
|
|
50
|
+
readonly supportsTextInputActions: boolean;
|
|
51
|
+
readonly supportsDoNotDisturbBypass: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Whether a notification's alert sound can be made to repeat until handled.
|
|
54
|
+
*
|
|
55
|
+
* Decides two things: whether a refused repeat is reported to the caller, and
|
|
56
|
+
* whether a stop marker is placed at all. Where this is false no marker is
|
|
57
|
+
* created, so the platform's pending ceiling is untouched by the feature.
|
|
58
|
+
*/
|
|
59
|
+
readonly supportsRepeatingAlert: boolean;
|
|
60
|
+
/** Milliseconds a background action or silent payload may run for. */
|
|
61
|
+
readonly backgroundWindowMs: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What applying a tone declaration actually did.
|
|
66
|
+
*
|
|
67
|
+
* A boolean cannot carry this: a caller has to tell a label correction from a
|
|
68
|
+
* newly created channel from a no-op, and reporting a refresh as a creation
|
|
69
|
+
* would make every apply look like a write.
|
|
70
|
+
*/
|
|
71
|
+
export type ToneDeclarationOutcome = 'created' | 'labelsRefreshed' | 'unchanged';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A package-owned artifact scheduled to end a repeating alert.
|
|
75
|
+
*
|
|
76
|
+
* Never a consumer notification. It exists because no platform offers "stop the
|
|
77
|
+
* sound at time T but keep the notification", so something has to run at T.
|
|
78
|
+
*/
|
|
79
|
+
export interface RepeatStop {
|
|
80
|
+
readonly markerId: string;
|
|
81
|
+
/** The consumer notification whose alert this stops. */
|
|
82
|
+
readonly targetId: string;
|
|
83
|
+
readonly dueAt: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A notification arriving, as opposed to a user acting on one. */
|
|
87
|
+
export interface DeliveryEvent {
|
|
88
|
+
readonly notificationId: string;
|
|
89
|
+
readonly occurredAt: number;
|
|
90
|
+
/** Present when this delivery is a package-owned stop marker rather than a real notification. */
|
|
91
|
+
readonly repeatStop?: RepeatStop;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface NotificationEngineAdapter {
|
|
95
|
+
readonly name: string;
|
|
96
|
+
|
|
97
|
+
getCapabilities(): Promise<EngineCapabilities>;
|
|
98
|
+
|
|
99
|
+
/** Scheduling an existing identifier replaces it; exactly one entry results. */
|
|
100
|
+
schedule(request: EngineScheduleRequest): Promise<void>;
|
|
101
|
+
|
|
102
|
+
cancel(id: string): Promise<void>;
|
|
103
|
+
cancelMany(ids: readonly string[]): Promise<void>;
|
|
104
|
+
cancelAll(): Promise<void>;
|
|
105
|
+
|
|
106
|
+
/** Full detail, so callers can diff rather than merely count. */
|
|
107
|
+
getPending(): Promise<readonly PendingEntry[]>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Applies a channel or category declaration.
|
|
111
|
+
*
|
|
112
|
+
* Must return `labelsRefreshed` only when a label actually changed. Returning
|
|
113
|
+
* it unconditionally would turn every apply into a write and quietly break
|
|
114
|
+
* the zero-operation guarantee reconciliation idempotence rests on.
|
|
115
|
+
*/
|
|
116
|
+
declareTone(tone: ResolvedTone): Promise<ToneDeclarationOutcome>;
|
|
117
|
+
/** Removes a retired channel. Returns true when one was actually removed. */
|
|
118
|
+
retireTone(channelId: string): Promise<boolean>;
|
|
119
|
+
declareGroups(groups: readonly ChannelGroup[]): Promise<void>;
|
|
120
|
+
/** Channel ids the platform currently holds, which re-declaration cannot reveal. */
|
|
121
|
+
listDeclaredChannels(): Promise<readonly string[]>;
|
|
122
|
+
|
|
123
|
+
getPermission(kind: PermissionKind): Promise<PermissionState>;
|
|
124
|
+
requestPermission(kind: PermissionKind): Promise<PermissionState>;
|
|
125
|
+
openSettings(kind: PermissionKind): Promise<void>;
|
|
126
|
+
getRestrictions(): Promise<RestrictionReport>;
|
|
127
|
+
|
|
128
|
+
/** Interactions arriving before a handler registers are the caller's problem to buffer, not the engine's. */
|
|
129
|
+
subscribeToInteractions(handler: (event: InteractionEvent) => void): Unsubscribe;
|
|
130
|
+
/** Any interaction that launched the app, consumed exactly once. */
|
|
131
|
+
takeInitialInteraction(): Promise<InteractionEvent | undefined>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Stops a delivered notification's repeating alert without removing it.
|
|
135
|
+
*
|
|
136
|
+
* Re-posts the notification under the same identifier with the alert
|
|
137
|
+
* suppressed. The same identifier means the same tray slot, so it replaces in
|
|
138
|
+
* place: the sound stops and the notification stays. Cancelling instead would
|
|
139
|
+
* be worse than never alerting at all — the user comes home to a silent phone
|
|
140
|
+
* and no idea what they missed.
|
|
141
|
+
*
|
|
142
|
+
* Returns false when the notification is no longer displayed, which is the
|
|
143
|
+
* ordinary case once the user has handled it.
|
|
144
|
+
*/
|
|
145
|
+
silenceDelivered(id: string): Promise<boolean>;
|
|
146
|
+
|
|
147
|
+
/** Package-owned stop markers currently scheduled. Never consumer notifications. */
|
|
148
|
+
listRepeatStops(): Promise<readonly RepeatStop[]>;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Deliveries, as distinct from interactions.
|
|
152
|
+
*
|
|
153
|
+
* `subscribeToInteractions` deliberately discards delivery events, because
|
|
154
|
+
* reporting an arrival as a tap would cancel a notification the user never
|
|
155
|
+
* touched. Repeat expiry needs exactly the events that filter throws away, so
|
|
156
|
+
* it gets its own subscription rather than loosening that one.
|
|
157
|
+
*/
|
|
158
|
+
subscribeToDeliveries(handler: (event: DeliveryEvent) => void): Unsubscribe;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Registers the handler that runs when the app process is not alive.
|
|
162
|
+
*
|
|
163
|
+
* Must be called at module scope, outside the React tree, because that is
|
|
164
|
+
* where the engine registers its headless task. The package cannot do this on
|
|
165
|
+
* a consumer's behalf, which is why it is a documented setup step rather than
|
|
166
|
+
* an internal detail — and why its absence is reported rather than guessed at.
|
|
167
|
+
*/
|
|
168
|
+
registerBackgroundDeliveryHandler(handler: (event: DeliveryEvent) => Promise<void>): void;
|
|
169
|
+
|
|
170
|
+
/** Whether a background delivery handler has been registered in this process. */
|
|
171
|
+
hasBackgroundDeliveryHandler(): boolean;
|
|
172
|
+
}
|