@chipmobilesdk/rn-notification 0.4.1 → 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +108 -12
  3. package/package.json +10 -2
  4. package/skills/sdk-rn-notification/SKILL.md +6 -5
  5. package/src/advanced.ts +35 -0
  6. package/src/channel/migrate.ts +176 -158
  7. package/src/channel/tone-map.ts +2 -10
  8. package/src/contract/errors.ts +21 -6
  9. package/src/contract/hash.ts +26 -0
  10. package/src/diagnostics/events.ts +10 -1
  11. package/src/diagnostics/redact.ts +9 -26
  12. package/src/diagnostics/snapshot.ts +34 -1
  13. package/src/engine/adapter.ts +21 -1
  14. package/src/engine/notify-kit/events.ts +155 -0
  15. package/src/engine/notify-kit/module.ts +148 -0
  16. package/src/engine/notify-kit/platform/android.ts +172 -0
  17. package/src/engine/notify-kit/platform/index.ts +55 -0
  18. package/src/engine/notify-kit/platform/ios.ts +54 -0
  19. package/src/engine/notify-kit/platform/types.ts +40 -0
  20. package/src/engine/notify-kit-engine.ts +96 -468
  21. package/src/index.ts +9 -0
  22. package/src/initialize.ts +10 -3
  23. package/src/notifications.ts +195 -0
  24. package/src/permission/index.ts +65 -4
  25. package/src/push/fcm-provider.ts +28 -11
  26. package/src/push/gate.ts +27 -5
  27. package/src/reconcile/diff.ts +38 -27
  28. package/src/reconcile/index.ts +13 -4
  29. package/src/recurrence/index.ts +2 -1
  30. package/src/repeat/expiry.ts +2 -6
  31. package/src/repeat/index.ts +45 -1
  32. package/src/repeat/marker.ts +1 -22
  33. package/src/repeat/place.ts +74 -35
  34. package/src/schedule/anchor.ts +35 -19
  35. package/src/schedule/index.ts +10 -4
  36. package/src/schedule/partition.ts +0 -19
  37. package/src/schedule/validate-identifier.ts +8 -6
  38. package/src/schedule/writer.ts +13 -4
  39. package/src/state/caps.ts +28 -3
  40. package/src/state/interaction-buffer.ts +30 -2
  41. package/src/state/mmkv-store.ts +54 -37
  42. package/src/state/permission-history.ts +25 -0
  43. package/src/state/record.ts +114 -20
  44. package/src/state/store.ts +3 -0
  45. package/src/testing/fake-engine.ts +17 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,161 @@
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
+
76
+ ## 0.5.0
77
+
78
+ ### Minor Changes
79
+
80
+ - Privacy (NOT-14), on top of `NotificationError` now extending `SdkError` from the new
81
+ `@chipmobilesdk/rn-core` dependency: ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
82
+
83
+ - Text the user types into a reply action is no longer written to disk. It is held in
84
+ memory, so a handler registered later in the same process still receives
85
+ `inputText`; after process death the event arrives without it.
86
+ - A state snapshot reports pending entries with their content and routing values
87
+ reduced to their shape (`[string:12]`). Pass `verbose: () => true` to
88
+ `createSnapshot` to include the values while debugging.
89
+ - A `NotificationError`'s `context`, and every diagnostic context, also has token-,
90
+ URL- and e-mail-shaped strings removed under any key.
91
+
92
+ - One diagnostic stream: install a sink with `installSdkSink` from
93
+ `@chipmobilesdk/rn-core` and it receives what every package observes (events,
94
+ failures, warnings) as `SdkDiagnostic`s, redacted. Listeners are notified through
95
+ rn-core, so a listener that throws no longer stops the others nor fails the call that
96
+ notified, and its error is reported rather than swallowed. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
97
+
98
+ - rn-ads: a banner's auto-refresh is a new cycle, so its `loaded` and `impression`
99
+ events are no longer deduplicated away; the dedupe memory is bounded. A throwing
100
+ status, consent or placement listener is reported as an `internal_error` event.
101
+ - rn-i18n: new `onDiagnostic` config receives every diagnostic (`I18nDiagnostic`,
102
+ with a stable `code`); without it, warnings go to `console.warn` in development
103
+ builds only. The warn-once memory is bounded.
104
+ - rn-chart: now depends on rn-core for its diagnostics.
105
+ - rn-payment: status, entitlement and pending listeners report a throw to the logger
106
+ instead of swallowing it.
107
+
108
+ - Native peers load through `lazyPeer` from `@chipmobilesdk/rn-core`, on first use,
109
+ and a missing one is each package's own typed error. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
110
+
111
+ - rn-auth: creating `auth` no longer loads react-native-app-auth or
112
+ react-native-keychain; each loads on first use. New entrypoints
113
+ `@chipmobilesdk/rn-auth/app-auth` and `/keychain`. The default platform comes from
114
+ rn-core.
115
+ - rn-camera: a preview whose VisionCamera feed cannot load fails the session with
116
+ `cameraUnavailable` (new `controller.reportSurfaceUnavailable()`), instead of
117
+ showing black with no error. A missing VisionCamera module is `cameraUnavailable`.
118
+ - rn-notification: `createMmkvStore` reports a fallback to memory as
119
+ `STATE_STORE_DEGRADED` (to the SDK sinks and a new `onDegraded` option) and
120
+ exposes `durable`.
121
+ - rn-payment: an iOS app validates its catalog against the iOS product ids; the
122
+ default platform used to be Android everywhere. A missing react-native-iap is
123
+ `PAYMENT_UNAVAILABLE`.
124
+ - rn-theme: `buildThemes` runs without React Native (new `hairlineWidth` option) and
125
+ importing the package no longer loads Unistyles; `applyThemeMode` loads it on
126
+ first use.
127
+ - rn-ads, rn-local-db: the same loading, with the same error codes as before.
128
+
129
+ - Formatters come from rn-core's cache instead of being built per call. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
130
+
131
+ - rn-notification: resolving a time zone builds one `Intl.DateTimeFormat` per zone
132
+ instead of one per occurrence; expanding 32 series of 32 occurrences went from
133
+ about 230 ms to under 30 ms on Node. Zone validity is memoized.
134
+ - rn-i18n: number, currency and date formatters are cached. A bad option (an unknown
135
+ currency such as `usd1`) goes through the invalid-format policy instead of
136
+ throwing out of a render, and a locale's `defaultCurrency` must be a three-letter
137
+ ISO 4217 code at startup.
138
+
139
+ ### Patch Changes
140
+
141
+ - One app-lifecycle source, and a startup that needs no extra frame. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
142
+
143
+ - rn-core: `appLifecycle()`, a shared, reference-counted `AppState` subscription that
144
+ reports changes only (`active`, `inactive`, `background`), with `onForeground` for a
145
+ return from the background and `createManualLifecycle` in `./testing`.
146
+ - rn-camera, rn-ads and rn-notification hear the lifecycle through it instead of
147
+ subscribing to `AppState` each, so an app holds one native listener. The camera
148
+ controller's core layer no longer imports `react-native`.
149
+ - rn-payment: `syncOnForeground(engine)` and `<PaymentProvider syncOnForeground>` call
150
+ `handleForeground()` on each return from the background. Off by default; calling
151
+ `handleForeground()` yourself works as before.
152
+ - rn-i18n: `i18n.initSync()` resolves the locale before the first render when the
153
+ storage adapter has the new optional `getItemSync`, and returns false otherwise.
154
+ `localeStorageFromKeyValue(port)` builds such an adapter from rn-core's
155
+ `KeyValuePort` (MMKV).
156
+ - Updated dependencies ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17)):
157
+ - @chipmobilesdk/rn-core@0.1.0
158
+
3
159
  ## 0.4.1
4
160
 
5
161
  ### Patch 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
- The steps themselves need inputs only your app has — the series rules it registered, the identifiers it expects, its permission module — so you register them once on `runtime.registry` after `initialize()`:
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 { runtime } = await initialize(config, { engine, store });
157
- runtime.registry.register(createReAnchorStep({ engine, timeZone, resolveTone, canScheduleExactly }));
158
- runtime.registry.register(createWindowRefreshStep({ recurrence, registeredRules: () => myRules }));
159
- runtime.registry.register(createVerifyStep({ engine, expectedIds: () => myExpectedIds }));
160
- runtime.registry.register(createPermissionRefreshStep({ permissions }));
161
- // On teardown (tests, a sign-out that rebuilds the runtime):
162
- runtime.dispose();
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,28 @@ notifications.onInteraction(async event => {
310
373
  });
311
374
  ```
312
375
 
313
- One handler, identical shape in foreground, background, and cold start. Events that arrive before your handler registers are **buffered durably** — they survive process death, which is exactly what a cold-start tap is.
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.
396
+
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`.
314
398
 
315
399
  When the target is gone, call `reportTargetMissing()` and pick your own fallback. No platform error is ever shown to the user.
316
400
 
@@ -340,6 +424,14 @@ Foreground pushes are **never auto-displayed** — presentation is your decision
340
424
 
341
425
  Local schedules take precedence over push for the same identifier. A server that wants to override sends `cancel` then `schedule`.
342
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
+
343
435
  ## What is persisted
344
436
 
345
437
  Six collections, in app-private, backup-excluded storage:
@@ -353,7 +445,7 @@ Six collections, in app-private, backup-excluded storage:
353
445
  | Permission history | 64 | — |
354
446
  | Last reconcile summary | 1 | — |
355
447
 
356
- **No notification content, no push token, no desired set.** The interaction buffer is the sole content-bearing exception, and it evicts on delivery.
448
+ **No notification content, no push token, no desired set, no typed reply.** The interaction buffer is the sole content-bearing exception (its routing payload), and it evicts on delivery.
357
449
 
358
450
  Corrupt state is **discarded and rebuilt**, never blocking. What you lose: deduplication history and buffered interactions. What is unaffected: **every notification already scheduled with the OS**.
359
451
 
@@ -404,9 +496,13 @@ Delivery accuracy: within **60 s** with exact alarms; within **15 min** without,
404
496
 
405
497
  ## Diagnostic identifiers
406
498
 
407
- 43 stable identifiers across validation, reconciliation, permissions, channels, interaction, push, state, and volume. Every one is reachable through the fault injector. See `ALL_DIAGNOSTIC_CODES`.
499
+ `NotificationError` extends `SdkError` from `@chipmobilesdk/rn-core` (`isSdkError`, `error.pkg === 'rn-notification'`), and its `context` is redacted when the error is built: content keys become their shape, and other strings lose anything shaped like a token, URL or e-mail address.
500
+
501
+ The state snapshot (`createSnapshot`) reports the pending set entry by entry, but a snapshot is what gets pasted into a bug report, so each entry's content and routing values are reduced to their shape (`[string:12]`). Pass `verbose: () => true` to include the values while debugging.
502
+
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`.
408
504
 
409
- 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)`.
410
506
 
411
507
  ## License
412
508
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chipmobilesdk/rn-notification",
3
- "version": "0.4.1",
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",
@@ -31,9 +36,12 @@
31
36
  ],
32
37
  "sideEffects": false,
33
38
  "scripts": {
34
- "typecheck": "tsc -p tsconfig.json --noEmit",
39
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
35
40
  "pack:check": "npm pack --dry-run"
36
41
  },
42
+ "dependencies": {
43
+ "@chipmobilesdk/rn-core": "^0.1.0"
44
+ },
37
45
  "peerDependencies": {
38
46
  "@react-native-firebase/app": ">=26.0.0",
39
47
  "@react-native-firebase/messaging": ">=26.0.0",
@@ -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.** Maintenance steps registered on
39
- `runtime.registry` (window refresh, re-anchoring, verification, permission re-read) run
40
- on every foreground return, but the package never invents a desired set. Data changed
41
- and you did not call `reconcile()` means the device is stale, by design. The steps are
42
- not registered for you — they need your rules and expected ids.
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.
@@ -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';