@chipmobilesdk/rn-notification 0.5.0 → 0.6.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 +73 -0
- package/README.md +100 -10
- package/package.json +6 -1
- package/skills/sdk-rn-notification/SKILL.md +6 -5
- package/src/advanced.ts +35 -0
- package/src/channel/migrate.ts +176 -158
- package/src/channel/tone-map.ts +2 -10
- package/src/contract/errors.ts +7 -0
- package/src/contract/hash.ts +26 -0
- package/src/engine/adapter.ts +21 -1
- package/src/engine/notify-kit/events.ts +155 -0
- package/src/engine/notify-kit/module.ts +148 -0
- package/src/engine/notify-kit/platform/android.ts +172 -0
- package/src/engine/notify-kit/platform/index.ts +55 -0
- package/src/engine/notify-kit/platform/ios.ts +54 -0
- package/src/engine/notify-kit/platform/types.ts +40 -0
- package/src/engine/notify-kit-engine.ts +96 -477
- package/src/index.ts +9 -0
- package/src/notifications.ts +195 -0
- package/src/permission/index.ts +65 -4
- package/src/push/fcm-provider.ts +14 -4
- package/src/push/gate.ts +27 -5
- package/src/reconcile/diff.ts +38 -27
- package/src/reconcile/index.ts +13 -4
- package/src/recurrence/index.ts +2 -1
- package/src/repeat/expiry.ts +2 -6
- package/src/repeat/index.ts +45 -1
- package/src/repeat/marker.ts +1 -22
- package/src/repeat/place.ts +74 -35
- package/src/schedule/index.ts +10 -4
- package/src/schedule/partition.ts +0 -19
- package/src/schedule/validate-identifier.ts +8 -6
- package/src/schedule/writer.ts +13 -4
- package/src/state/caps.ts +28 -3
- package/src/state/permission-history.ts +25 -0
- package/src/state/record.ts +110 -20
- package/src/testing/fake-engine.ts +17 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# @chipmobilesdk/rn-notification
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- `createNotifications(config, deps?)` assembles the whole package in one call. ([#19](https://github.com/operationd8979/chip-mobile-sdk/pull/19))
|
|
8
|
+
|
|
9
|
+
- It returns the scheduler, reconciler, recurrence, permissions, channels, interactions
|
|
10
|
+
and snapshot, wired over one engine, one state store and the one redacting
|
|
11
|
+
diagnostic emitter, plus `onInteraction`, `runMaintenance`, `canScheduleExactly` and
|
|
12
|
+
`dispose`. The engine, store, clock and time zone default to the real ones and can
|
|
13
|
+
be injected.
|
|
14
|
+
- `canScheduleExactly` follows the exact-alarm permission the package last read,
|
|
15
|
+
instead of each app passing its own callback (often `() => true`).
|
|
16
|
+
- Automatic maintenance has its standard steps registered: re-anchoring after a
|
|
17
|
+
time-zone change and the permission re-read, plus recurrence window refresh when you
|
|
18
|
+
pass `recurrenceRules`.
|
|
19
|
+
- The individual factories stay exported from the root, and are also available from
|
|
20
|
+
the new `@chipmobilesdk/rn-notification/advanced` entrypoint for apps that wire the
|
|
21
|
+
package themselves.
|
|
22
|
+
- Push: `getAvailability()` no longer fetches a token (which registered the device
|
|
23
|
+
with FCM, and APNs on iOS, just to answer the question). The new
|
|
24
|
+
`gate.handleBackground(raw)` runs a background payload through the same
|
|
25
|
+
normalization and persisted deduplication as a foreground one. Several
|
|
26
|
+
`onTokenChange` subscriptions no longer leak each other, and `dispose()` releases
|
|
27
|
+
them all.
|
|
28
|
+
- The README starts from `createNotifications`; its quick start is checked against a
|
|
29
|
+
type-checked example in the tests.
|
|
30
|
+
|
|
31
|
+
- Fewer reads across the native bridge, storage written per collection, and one
|
|
32
|
+
pending budget for the whole app. ([#19](https://github.com/operationd8979/chip-mobile-sdk/pull/19))
|
|
33
|
+
|
|
34
|
+
- A reconcile, a group or owner cancel, and a tone apply each read the pending set
|
|
35
|
+
(or the channel list) once per pass instead of once per operation. Reconciling 100
|
|
36
|
+
changes now makes at most two full reads of the trigger list; it used to make one
|
|
37
|
+
or more per change. Scheduling one notification asks for identifiers only where the
|
|
38
|
+
engine can list them (`pendingIds`, optional on the engine port).
|
|
39
|
+
- Persisted state is stored under one key per collection. Recording a push
|
|
40
|
+
deduplication entry writes the dedup ledger alone, where every update used to
|
|
41
|
+
serialize the whole record. A record written by an earlier version is migrated on
|
|
42
|
+
first load.
|
|
43
|
+
- The series and migration ledgers no longer evict live entries at their cap. Only
|
|
44
|
+
exhausted series and retired channels are dropped; if live entries alone exceed the
|
|
45
|
+
cap they are all kept and the new `STATE_CAP_EXCEEDED` diagnostic says so. Evicting a
|
|
46
|
+
live entry reset a series' count and made a migrated tone look new.
|
|
47
|
+
- Reconciliation budgets a domain against the platform ceiling minus what other
|
|
48
|
+
domains, series and direct schedules already hold. Two domains could previously
|
|
49
|
+
overflow iOS's 64 pending notifications together while each reported no truncation.
|
|
50
|
+
|
|
51
|
+
- Three fixes: what a tone change cancels, background interactions, and permanent denial. ([#19](https://github.com/operationd8979/chip-mobile-sdk/pull/19))
|
|
52
|
+
|
|
53
|
+
- `channels.apply()` cancels a pending notification only when it is on a tone the
|
|
54
|
+
package applied earlier and the configuration no longer declares. It used to cancel
|
|
55
|
+
everything whose tone was not in the list passed to that call, which included
|
|
56
|
+
notifications scheduled by other code through the same engine.
|
|
57
|
+
- Taps, actions and dismissals that arrive while the app is in the background are
|
|
58
|
+
delivered. `registerBackgroundNotificationHandler` takes an optional `onInteraction`
|
|
59
|
+
callback and an optional `store`: a running app's `onInteraction` handlers get the
|
|
60
|
+
event with `appState: 'background'`; otherwise the callback does; with no callback, or
|
|
61
|
+
one that throws, the event is kept in the store and delivered on the next start.
|
|
62
|
+
Before, background actions such as "Taken" or "Snooze" never reached the app.
|
|
63
|
+
- `permanentlyDenied` is now reported: after one refused display request on iOS, two on
|
|
64
|
+
Android 13+, and `DISPLAY_PERMISSION_PERMANENT` is emitted. The count resets once the
|
|
65
|
+
permission is seen granted. After `requestExactAlarmPermission()` the package re-reads
|
|
66
|
+
the capability when the user returns from system settings and notifies
|
|
67
|
+
`onPermissionChange`.
|
|
68
|
+
|
|
69
|
+
### Patch Changes
|
|
70
|
+
|
|
71
|
+
- The notify-kit engine is split by platform. Android and iOS behaviour live in separate
|
|
72
|
+
strategy files chosen once when the engine is created, instead of about fifteen
|
|
73
|
+
`Platform.OS` branches in one 790-line file. `createNotifyKitEngine` accepts an optional
|
|
74
|
+
`platform` for testing one platform's behaviour. No behaviour change. ([#19](https://github.com/operationd8979/chip-mobile-sdk/pull/19))
|
|
75
|
+
|
|
3
76
|
## 0.5.0
|
|
4
77
|
|
|
5
78
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -17,6 +17,55 @@ Every notification feature re-solves the same problems, and most of them are dis
|
|
|
17
17
|
- Display permission and exact-alarm permission are **different things**, asked separately, and a refusal must not break the user's actual work.
|
|
18
18
|
- Android channels are **immutable once created** — changing one is a migration, not an assignment.
|
|
19
19
|
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
`createNotifications` assembles the whole package: scheduler, reconciler, recurrence,
|
|
23
|
+
permissions, channels, interactions, a diagnostic snapshot, and automatic maintenance.
|
|
24
|
+
It applies your tones, loads persisted state, and schedules exactly only while the
|
|
25
|
+
exact-alarm permission allows it.
|
|
26
|
+
|
|
27
|
+
<!-- quick-start -->
|
|
28
|
+
```ts
|
|
29
|
+
import { createNotifications } from '@chipmobilesdk/rn-notification';
|
|
30
|
+
|
|
31
|
+
export async function setUpNotifications(navigate: (routing: unknown) => void) {
|
|
32
|
+
const notifications = await createNotifications({
|
|
33
|
+
tones: [
|
|
34
|
+
{
|
|
35
|
+
id: 'reminder',
|
|
36
|
+
name: 'Reminders',
|
|
37
|
+
urgency: 'high',
|
|
38
|
+
sound: { enabled: true },
|
|
39
|
+
vibration: true,
|
|
40
|
+
badge: true,
|
|
41
|
+
version: 1,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await notifications.permissions.requestDisplayPermission();
|
|
47
|
+
|
|
48
|
+
await notifications.scheduler.schedule({
|
|
49
|
+
id: 'task:42:due', // derived from your data, never random
|
|
50
|
+
content: { title: 'Pay the rent', body: 'Due today' },
|
|
51
|
+
tone: 'reminder',
|
|
52
|
+
moment: { at: Date.parse('2026-12-01T08:00:00') },
|
|
53
|
+
routing: { screen: 'task', taskId: '42' },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
notifications.onInteraction(async event => {
|
|
57
|
+
if (event.kind === 'tap') navigate(event.routing);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return notifications;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
<!-- /quick-start -->
|
|
64
|
+
|
|
65
|
+
The individual factories (`createScheduler`, `createReconciler`, `initialize`, the
|
|
66
|
+
maintenance steps, ...) remain available, from the package root and from
|
|
67
|
+
`@chipmobilesdk/rn-notification/advanced`, for an app that needs a different wiring.
|
|
68
|
+
|
|
20
69
|
## Installation
|
|
21
70
|
|
|
22
71
|
```sh
|
|
@@ -150,18 +199,25 @@ The package re-anchors wall-clock notifications when it notices the device zone
|
|
|
150
199
|
| Applies `config.tones` (and any channel migration) during `initialize()` | `reconcile()` — the desired set |
|
|
151
200
|
| Runs every step on `runtime.registry` when the app returns to the foreground — with the trigger `timeZoneChange` when the device zone moved meanwhile | |
|
|
152
201
|
|
|
153
|
-
|
|
202
|
+
`createNotifications` registers the standard steps for you: re-anchoring after a
|
|
203
|
+
time-zone change, re-reading permissions, and — when you pass your recurrence rules —
|
|
204
|
+
refreshing their windows. Steps that need inputs only your app has are added on
|
|
205
|
+
`notifications.runtime.registry`:
|
|
154
206
|
|
|
155
207
|
```ts
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
runtime.registry.register(
|
|
160
|
-
runtime.
|
|
161
|
-
|
|
162
|
-
|
|
208
|
+
const notifications = await createNotifications(config, {
|
|
209
|
+
recurrenceRules: () => myRules, // the rules your app registered; the package never stores them
|
|
210
|
+
});
|
|
211
|
+
notifications.runtime.registry.register(
|
|
212
|
+
createVerifyStep({ engine: notifications.runtime.engine, expectedIds: () => myExpectedIds }),
|
|
213
|
+
);
|
|
214
|
+
// On teardown (tests, a sign-out that rebuilds everything):
|
|
215
|
+
notifications.dispose();
|
|
163
216
|
```
|
|
164
217
|
|
|
218
|
+
Wiring it yourself with `initialize()` and the step factories (from `./advanced`) still
|
|
219
|
+
works; then registering the steps is yours.
|
|
220
|
+
|
|
165
221
|
Before 0.4.0 nothing triggered these steps; they ran only when the app called `runMaintenance()` itself. A cycle that throws is reported as `MAINTENANCE_FAILED`, because nothing awaits it.
|
|
166
222
|
|
|
167
223
|
**Reconciliation never runs on its own.** Only you know what your data implies; a package that guessed would cancel notifications it does not understand.
|
|
@@ -298,6 +354,13 @@ await permissions.openSettings('display'); // per-permission
|
|
|
298
354
|
|
|
299
355
|
Six states, never collapsed: `notAsked`, `granted`, `denied`, `permanentlyDenied`, `provisional`, `notRequired`.
|
|
300
356
|
|
|
357
|
+
`permanentlyDenied` means the system will not show its prompt again, so offer Settings
|
|
358
|
+
(`shouldOfferSettings(state)`). The platform itself only ever says `denied`; the package
|
|
359
|
+
counts refusals — one on iOS, two on Android 13+ — and forgets them once the permission
|
|
360
|
+
is seen granted. The exact-alarm capability is granted on a system settings screen, so
|
|
361
|
+
`requestExactAlarmPermission()` answers with the state before the user chose; the real
|
|
362
|
+
answer is re-read when they return to the app and reaches `onPermissionChange`.
|
|
363
|
+
|
|
301
364
|
**Ask after intent, never on first launch.** A prompt at the wrong moment is a permanent denial.
|
|
302
365
|
|
|
303
366
|
**A refusal never fails your operation.** The user's work completes; the lost capability is reported separately.
|
|
@@ -310,7 +373,26 @@ notifications.onInteraction(async event => {
|
|
|
310
373
|
});
|
|
311
374
|
```
|
|
312
375
|
|
|
313
|
-
One handler, identical shape in foreground, background, and cold start.
|
|
376
|
+
One handler, identical shape in foreground, background, and cold start (`event.appState` says which).
|
|
377
|
+
|
|
378
|
+
**Background actions need the module-scope handler.** The engine has one background
|
|
379
|
+
handler per process, registered in `index.js`. Pass it your store and, optionally, a
|
|
380
|
+
callback:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
registerBackgroundNotificationHandler({
|
|
384
|
+
engine,
|
|
385
|
+
store: createMmkvStore(), // an interaction nobody handled is kept for the next start
|
|
386
|
+
onInteraction: async event => {
|
|
387
|
+
// Optional: handle "Taken" or "Snooze" right away, inside the background window.
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
While the app is running, a background tap or action goes to your `onInteraction`
|
|
393
|
+
handlers as usual. In a process started only for the event, it goes to the callback
|
|
394
|
+
above; without one (or if it throws) it is kept in the store and delivered when the app
|
|
395
|
+
next starts. Before 0.6.0 these events were dropped. Events that arrive before your handler registers are **buffered durably** — they survive process death, which is exactly what a cold-start tap is.
|
|
314
396
|
|
|
315
397
|
Text the user typed into a reply action (`inputText`) is the exception: it is free text, so it is **never written to disk**. It is held in memory beside the buffer, so a handler that registers later in the same process still receives it; if the process dies first, the event is delivered without `inputText`.
|
|
316
398
|
|
|
@@ -342,6 +424,14 @@ Foreground pushes are **never auto-displayed** — presentation is your decision
|
|
|
342
424
|
|
|
343
425
|
Local schedules take precedence over push for the same identifier. A server that wants to override sends `cancel` then `schedule`.
|
|
344
426
|
|
|
427
|
+
## Channel changes and notifications already scheduled
|
|
428
|
+
|
|
429
|
+
`channels.apply()` cancels a pending notification only when it sits on a tone this
|
|
430
|
+
package applied earlier and the configuration no longer declares. Notifications
|
|
431
|
+
scheduled by other code through the same engine, and tones the package has never
|
|
432
|
+
applied, are left alone (0.6.0; before, anything not in the list passed to `apply()` was
|
|
433
|
+
cancelled).
|
|
434
|
+
|
|
345
435
|
## What is persisted
|
|
346
436
|
|
|
347
437
|
Six collections, in app-private, backup-excluded storage:
|
|
@@ -412,7 +502,7 @@ The state snapshot (`createSnapshot`) reports the pending set entry by entry, bu
|
|
|
412
502
|
|
|
413
503
|
45 stable identifiers across validation, reconciliation, permissions, channels, interaction, push, state, and volume. Every one is reachable through the fault injector. See `ALL_DIAGNOSTIC_CODES`.
|
|
414
504
|
|
|
415
|
-
Some are **reported outcomes, not failures** — the operation succeeded and something you should know happened alongside it: `TRUNCATED_OVER_CEILING`, `EXACT_ALARM_UNAVAILABLE`, `TARGET_MISSING`, `PENDING_SET_DISCREPANCY`, `BURST_THRESHOLD_EXCEEDED`, `PAYLOAD_VERSION_SKEW`, `PAYLOAD_DUPLICATE`, `STATE_CAP_EVICTED`, `STATE_CORRUPT_REBUILT`, `DND_BYPASS_UNAVAILABLE`, `TONE_LABELS_REFRESHED`, `REPEAT_ALERT_UNAVAILABLE`, `REPEAT_STOP_HANDLER_MISSING`. Use `isNonFailure(code)`.
|
|
505
|
+
Some are **reported outcomes, not failures** — the operation succeeded and something you should know happened alongside it: `TRUNCATED_OVER_CEILING`, `EXACT_ALARM_UNAVAILABLE`, `TARGET_MISSING`, `PENDING_SET_DISCREPANCY`, `BURST_THRESHOLD_EXCEEDED`, `PAYLOAD_VERSION_SKEW`, `PAYLOAD_DUPLICATE`, `STATE_CAP_EVICTED`, `STATE_CAP_EXCEEDED`, `STATE_CORRUPT_REBUILT`, `DND_BYPASS_UNAVAILABLE`, `TONE_LABELS_REFRESHED`, `REPEAT_ALERT_UNAVAILABLE`, `REPEAT_STOP_HANDLER_MISSING`. Use `isNonFailure(code)`.
|
|
416
506
|
|
|
417
507
|
## License
|
|
418
508
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chipmobilesdk/rn-notification",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"react-native": "./src/index.ts",
|
|
12
12
|
"default": "./src/index.ts"
|
|
13
13
|
},
|
|
14
|
+
"./advanced": {
|
|
15
|
+
"types": "./src/advanced.ts",
|
|
16
|
+
"react-native": "./src/advanced.ts",
|
|
17
|
+
"default": "./src/advanced.ts"
|
|
18
|
+
},
|
|
14
19
|
"./push": {
|
|
15
20
|
"types": "./src/push/index.ts",
|
|
16
21
|
"react-native": "./src/push/index.ts",
|
|
@@ -35,11 +35,12 @@ Three things cause most of the bugs, and none is visible in the type signatures:
|
|
|
35
35
|
2. **Wall-clock is the default anchoring mode** — an 08:00 reminder stays 08:00 after the
|
|
36
36
|
user changes time zone. One case re-anchoring cannot fix: read
|
|
37
37
|
`### What re-anchoring cannot fix` before promising exact behaviour to anyone.
|
|
38
|
-
3. **Reconciliation is never automatic.**
|
|
39
|
-
|
|
40
|
-
on every foreground return
|
|
41
|
-
and you did not call `reconcile()` means the device is stale, by
|
|
42
|
-
|
|
38
|
+
3. **Reconciliation is never automatic.** Start from `createNotifications(config, deps)`:
|
|
39
|
+
it registers re-anchoring and the permission re-read, plus window refresh when you pass
|
|
40
|
+
`recurrenceRules`, and runs them on every foreground return. It never invents a desired
|
|
41
|
+
set: data changed and you did not call `reconcile()` means the device is stale, by
|
|
42
|
+
design. Wiring with `initialize()` and the factories from `./advanced` leaves step
|
|
43
|
+
registration to you.
|
|
43
44
|
|
|
44
45
|
Read `## Choosing identifiers`, `## Choosing an anchoring mode`, and
|
|
45
46
|
`## Maintenance versus reconciliation` before designing anything.
|
package/src/advanced.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@chipmobilesdk/rn-notification/advanced`: the individual factories, for an app
|
|
3
|
+
* that wires the package itself instead of using `createNotifications` (NOT-07).
|
|
4
|
+
*
|
|
5
|
+
* The package root still exports them too, for compatibility; new code that needs
|
|
6
|
+
* them should import from here, where they are documented as the advanced path.
|
|
7
|
+
*/
|
|
8
|
+
export { initialize } from './initialize';
|
|
9
|
+
export type { NotificationRuntime, RuntimeDependencies } from './initialize';
|
|
10
|
+
export { createScheduler } from './schedule';
|
|
11
|
+
export type { Scheduler, ScheduleDependencies } from './schedule';
|
|
12
|
+
export { createReconciler } from './reconcile';
|
|
13
|
+
export type { Reconciler, ReconcileDependencies } from './reconcile';
|
|
14
|
+
export { createRecurrence } from './recurrence';
|
|
15
|
+
export type { Recurrence, RecurrenceDependencies } from './recurrence';
|
|
16
|
+
export { createChannels } from './channel/migrate';
|
|
17
|
+
export type { ChannelDependencies, Channels } from './channel/migrate';
|
|
18
|
+
export { createToneResolver } from './channel/tone-map';
|
|
19
|
+
export { createInteractions } from './interaction';
|
|
20
|
+
export type { InteractionDependencies, Interactions } from './interaction';
|
|
21
|
+
export { createSnapshot } from './diagnostics/snapshot';
|
|
22
|
+
export type { Snapshot, SnapshotDependencies } from './diagnostics/snapshot';
|
|
23
|
+
export { createBurstDetector } from './diagnostics/burst';
|
|
24
|
+
export { createPermissions } from './permission';
|
|
25
|
+
export type { PermissionDependencies, Permissions } from './permission';
|
|
26
|
+
export { createPermissionRefreshStep } from './maintenance/permission-refresh';
|
|
27
|
+
export { createReAnchorStep } from './maintenance/reanchor';
|
|
28
|
+
export { createVerifyStep } from './maintenance/verify';
|
|
29
|
+
export { createWindowRefreshStep } from './maintenance/window-refresh';
|
|
30
|
+
export { MaintenanceController } from './maintenance/toggle';
|
|
31
|
+
export { MaintenanceRegistry } from './maintenance/cycle';
|
|
32
|
+
export type { MaintenanceContext, MaintenanceStep, MaintenanceTrigger } from './maintenance/cycle';
|
|
33
|
+
export { StateManager } from './state/record';
|
|
34
|
+
export { createMmkvStore } from './state/mmkv-store';
|
|
35
|
+
export { createPlatformTimeZoneReader } from './schedule/anchor';
|