@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +257 -166
  2. package/README.md +420 -410
  3. package/package.json +61 -75
  4. package/skills/sdk-rn-notification/SKILL.md +172 -173
  5. package/src/channel/migrate.ts +285 -289
  6. package/src/channel/tone-map.ts +130 -130
  7. package/src/channel/validate-tone.ts +61 -61
  8. package/src/contract/channel.ts +192 -188
  9. package/src/contract/diagnostics.ts +102 -102
  10. package/src/contract/errors.ts +142 -134
  11. package/src/contract/index.ts +10 -10
  12. package/src/contract/interaction.ts +62 -62
  13. package/src/contract/notification.ts +118 -118
  14. package/src/contract/permission.ts +55 -60
  15. package/src/contract/reconcile.ts +88 -88
  16. package/src/contract/recurrence.ts +129 -129
  17. package/src/diagnostics/burst.ts +101 -101
  18. package/src/diagnostics/events.ts +81 -72
  19. package/src/diagnostics/redact.ts +55 -72
  20. package/src/diagnostics/snapshot.ts +100 -64
  21. package/src/engine/adapter.ts +172 -172
  22. package/src/engine/codec.ts +89 -89
  23. package/src/engine/notify-kit-engine.ts +715 -707
  24. package/src/index.ts +208 -208
  25. package/src/initialize.ts +247 -236
  26. package/src/interaction/index.ts +146 -146
  27. package/src/maintenance/cycle.ts +150 -154
  28. package/src/maintenance/permission-refresh.ts +39 -41
  29. package/src/maintenance/reanchor.ts +101 -101
  30. package/src/maintenance/toggle.ts +66 -64
  31. package/src/maintenance/verify.ts +83 -83
  32. package/src/maintenance/window-refresh.ts +64 -64
  33. package/src/permission/index.ts +183 -183
  34. package/src/permission/state.ts +77 -77
  35. package/src/push/envelope.ts +165 -165
  36. package/src/push/fcm-provider.ts +150 -146
  37. package/src/push/gate.ts +190 -190
  38. package/src/push/index.ts +33 -29
  39. package/src/push/instructions.ts +114 -109
  40. package/src/push/provider-adapter.ts +54 -54
  41. package/src/push/signout.ts +65 -65
  42. package/src/reconcile/chunk.ts +53 -53
  43. package/src/reconcile/diff.ts +142 -145
  44. package/src/reconcile/duplicates.ts +73 -73
  45. package/src/reconcile/index.ts +180 -180
  46. package/src/reconcile/truncate.ts +56 -56
  47. package/src/recurrence/clock-guard.ts +73 -71
  48. package/src/recurrence/expand.ts +326 -319
  49. package/src/recurrence/index.ts +270 -259
  50. package/src/repeat/expiry.ts +53 -55
  51. package/src/repeat/index.ts +54 -54
  52. package/src/repeat/marker.ts +72 -72
  53. package/src/repeat/place.ts +73 -73
  54. package/src/schedule/anchor.ts +294 -267
  55. package/src/schedule/index.ts +174 -181
  56. package/src/schedule/partition.ts +88 -85
  57. package/src/schedule/resolve-moment.ts +80 -80
  58. package/src/schedule/validate-content.ts +111 -111
  59. package/src/schedule/validate-identifier.ts +126 -126
  60. package/src/schedule/writer.ts +55 -55
  61. package/src/state/caps.ts +71 -71
  62. package/src/state/dedup-ledger.ts +35 -35
  63. package/src/state/interaction-buffer.ts +101 -73
  64. package/src/state/mmkv-store.ts +92 -75
  65. package/src/state/permission-history.ts +41 -41
  66. package/src/state/record.ts +270 -263
  67. package/src/state/reset.ts +41 -41
  68. package/src/state/series-ledger.ts +91 -91
  69. package/src/state/store.ts +43 -40
  70. package/src/testing/conformance/index.ts +258 -248
  71. package/src/testing/fake-engine.ts +462 -468
  72. package/src/testing/fake-provider.ts +126 -129
  73. package/src/testing/faults.ts +72 -72
  74. package/src/testing/index.ts +23 -28
  75. package/src/testing/measure.ts +337 -337
package/package.json CHANGED
@@ -1,75 +1,61 @@
1
- {
2
- "name": "@chipmobilesdk/rn-notification",
3
- "version": "0.4.0",
4
- "private": false,
5
- "license": "UNLICENSED",
6
- "main": "src/index.ts",
7
- "types": "src/index.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./src/index.ts",
11
- "react-native": "./src/index.ts",
12
- "default": "./src/index.ts"
13
- },
14
- "./push": {
15
- "types": "./src/push/index.ts",
16
- "react-native": "./src/push/index.ts",
17
- "default": "./src/push/index.ts"
18
- },
19
- "./testing": {
20
- "types": "./src/testing/index.ts",
21
- "react-native": "./src/testing/index.ts",
22
- "default": "./src/testing/index.ts"
23
- }
24
- },
25
- "files": [
26
- "src/index.ts",
27
- "src/initialize.ts",
28
- "src/channel",
29
- "src/contract",
30
- "src/diagnostics",
31
- "src/engine",
32
- "src/interaction",
33
- "src/maintenance",
34
- "src/permission",
35
- "src/push",
36
- "src/recurrence",
37
- "src/repeat",
38
- "src/reconcile",
39
- "src/schedule",
40
- "src/state",
41
- "src/testing",
42
- "skills",
43
- "README.md",
44
- "CHANGELOG.md"
45
- ],
46
- "sideEffects": false,
47
- "scripts": {
48
- "typecheck": "tsc -p tsconfig.json --noEmit",
49
- "pack:dry-run": "npm pack --dry-run"
50
- },
51
- "peerDependencies": {
52
- "@react-native-firebase/app": ">=26.0.0",
53
- "@react-native-firebase/messaging": ">=26.0.0",
54
- "react": ">=19",
55
- "react-native": ">=0.85",
56
- "react-native-localize": ">=3.7.0",
57
- "react-native-mmkv": ">=4.0.0",
58
- "react-native-notify-kit": ">=10.5.0"
59
- },
60
- "peerDependenciesMeta": {
61
- "@react-native-firebase/app": {
62
- "optional": true
63
- },
64
- "@react-native-firebase/messaging": {
65
- "optional": true
66
- }
67
- },
68
- "engines": {
69
- "node": ">=22.11.0"
70
- },
71
- "publishConfig": {
72
- "access": "public",
73
- "registry": "https://registry.npmjs.org/"
74
- }
75
- }
1
+ {
2
+ "name": "@chipmobilesdk/rn-notification",
3
+ "version": "0.5.0",
4
+ "private": false,
5
+ "license": "UNLICENSED",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "react-native": "./src/index.ts",
12
+ "default": "./src/index.ts"
13
+ },
14
+ "./push": {
15
+ "types": "./src/push/index.ts",
16
+ "react-native": "./src/push/index.ts",
17
+ "default": "./src/push/index.ts"
18
+ },
19
+ "./testing": {
20
+ "types": "./src/testing/index.ts",
21
+ "react-native": "./src/testing/index.ts",
22
+ "default": "./src/testing/index.ts"
23
+ }
24
+ },
25
+ "files": [
26
+ "src",
27
+ "!src/**/__tests__",
28
+ "skills",
29
+ "README.md",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "sideEffects": false,
33
+ "scripts": {
34
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
35
+ "pack:check": "npm pack --dry-run"
36
+ },
37
+ "dependencies": {
38
+ "@chipmobilesdk/rn-core": "^0.1.0"
39
+ },
40
+ "peerDependencies": {
41
+ "@react-native-firebase/app": ">=26.0.0",
42
+ "@react-native-firebase/messaging": ">=26.0.0",
43
+ "react": ">=19",
44
+ "react-native": ">=0.85",
45
+ "react-native-localize": ">=3.7.0",
46
+ "react-native-mmkv": ">=4.0.0",
47
+ "react-native-notify-kit": ">=10.5.0"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@react-native-firebase/app": {
51
+ "optional": true
52
+ },
53
+ "@react-native-firebase/messaging": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "publishConfig": {
58
+ "access": "public",
59
+ "registry": "https://registry.npmjs.org/"
60
+ }
61
+ }
@@ -1,173 +1,172 @@
1
- ---
2
- name: sdk-rn-notification
3
- description: "Local and push notifications via @chipmobilesdk/rn-notification: scheduling, identifiers, wall-clock vs absolute anchoring, idempotent reconciliation, recurrence windows, permissions, alarm-style repeating tones with a bounded window, tone labels in system settings, Android channel migration, tap routing, FCM. Use when scheduling a reminder or alarm, making a notification repeat until handled, naming a tone users see in settings, handling a notification tap, requesting notification permission, changing a channel, or debugging a notification that did not arrive."
4
- metadata:
5
- package: "@chipmobilesdk/rn-notification"
6
- packageVersion: "0.3.0"
7
- user-invocable: true
8
- disable-model-invocation: false
9
- ---
10
-
11
- # @chipmobilesdk/rn-notification
12
-
13
- Scheduled local notifications with stable identity and idempotent reconciliation,
14
- provider-agnostic push, permissions, declarative channels and tones, and tap routing that
15
- survives a cold start.
16
-
17
- ## Use this when
18
-
19
- - Scheduling, replacing, or cancelling a notification
20
- - Reacting to a tap, an action button, or a dismissal
21
- - Requesting notification or exact-alarm permission
22
- - Declaring or changing a tone/channel, or enabling push
23
- - Building an alarm-style reminder that repeats until the user handles it
24
- - Debugging a notification that did not arrive, or arrived twice
25
-
26
- **Not** for in-app banners or an inbox (this package renders nothing), for storing app
27
- data (`@chipmobilesdk/rn-local-db`), or for translating text — resolve strings yourself.
28
-
29
- ## Read before writing any code
30
-
31
- Three things cause most of the bugs, and none is visible in the type signatures:
32
-
33
- 1. **Identifiers are yours and must be derived from data.** `crypto.randomUUID()` means
34
- you can never replace or cancel what you scheduled. Nothing is normalized, so
35
- `Task:1` and `task:1` are two different notifications.
36
- 2. **Wall-clock is the default anchoring mode** — an 08:00 reminder stays 08:00 after the
37
- user changes time zone. One case re-anchoring cannot fix: read
38
- `### What re-anchoring cannot fix` before promising exact behaviour to anyone.
39
- 3. **Reconciliation is never automatic.** Maintenance steps registered on
40
- `runtime.registry` (window refresh, re-anchoring, verification, permission re-read) run
41
- on every foreground return, but the package never invents a desired set. Data changed
42
- and you did not call `reconcile()` means the device is stale, by design. The steps are
43
- not registered for you — they need your rules and expected ids.
44
-
45
- Read `## Choosing identifiers`, `## Choosing an anchoring mode`, and
46
- `## Maintenance versus reconciliation` before designing anything.
47
-
48
- **A refusal must never break the user's work.** Permission denial is a degradation, not a
49
- failure: the caller's operation still succeeds and the lost capability is reported
50
- separately. Without exact-alarm capability notifications are still scheduled, flagged
51
- `mayBeDelayed`. Never gate a user action on a permission answer.
52
-
53
- ## Android: two manifest entries you must add yourself
54
-
55
- Consumer-owned; this package cannot ship either.
56
-
57
- - **`SCHEDULE_EXACT_ALARM` arrives whether you want it or not** — the engine declares it
58
- and manifest merging is transitive. It is store-restricted and needs a Play Console
59
- declaration. Remove it with `tools:node="remove"` unless you need precise timing.
60
- - **Backup exclusion** for the state record — a deduplication ledger restored onto another
61
- device claims payloads were already delivered, silently suppressing real ones.
62
-
63
- See `### Android` and `### Backup exclusion`.
64
-
65
- ## Public API
66
-
67
- Three entrypoints. The **root** exports one factory per concern (`createScheduler`,
68
- `createReconciler`, `createRecurrence`, `createPermissions`, `createChannels`,
69
- `createInteractions`, `createSnapshot`), the maintenance steps and their registry, plus
70
- `resolveTone`, `registerBackgroundNotificationHandler`, `resetState`, `clearOwner`,
71
- `isNonFailure`, `REPEAT_ALERT_BOUNDS` and `DIAGNOSTIC_CODES`.
72
- **`/push`** carries the push gate, FCM provider, instruction handler and sign-out —
73
- **import it only if you enable push**, because an app that never touches this subpath ships
74
- no provider code, no push permissions, and no entitlements. **`/testing`** carries the fake
75
- engine and provider, the conformance suite and the fault injector.
76
-
77
- Enumerate exact names from `node_modules/@chipmobilesdk/rn-notification/src/index.ts`
78
- rather than guessing.
79
-
80
- ## Where the detail lives
81
-
82
- `node_modules/@chipmobilesdk/rn-notification/README.md`:
83
-
84
- | Need | Section |
85
- |---|---|
86
- | Install, peers, enabling push | `## Installation`, `### Enabling push (optional)` |
87
- | Native setup | `### iOS`, `### Android`, `### Backup exclusion` |
88
- | Naming notifications | `## Choosing identifiers` |
89
- | Tones, alarms, labels, version bumps | `## Tones and channels` |
90
- | Time zones and travel | `## Choosing an anchoring mode` |
91
- | Automatic vs your responsibility | `## Maintenance versus reconciliation` |
92
- | Converging the device to your data | `## Reconciliation`, `## Recurrence` |
93
- | Permission timing and recovery | `## Permissions` |
94
- | Taps, actions, cold start | `## Interaction` |
95
- | Push and the backend contract | `## Push` |
96
- | On-device storage and privacy | `## What is persisted`, `## Data behaviour …` |
97
- | Limits and timing budgets | `## Budgets` |
98
- | Error codes | `## Diagnostic identifiers` |
99
-
100
- ## Alarm-style tones (0.2.0+)
101
-
102
- For a reminder that must not be missed — medication, a wake-up, an appointment — declare
103
- the repeat on the **tone**, not on each notification:
104
-
105
- ```ts
106
- await channels.apply([{
107
- id: 'medication',
108
- name: t('notifications.medication.name'), // localized; users read this in settings
109
- description: t('notifications.medication.hint'),
110
- urgency: 'max',
111
- sound: { name: 'chime' },
112
- vibration: true,
113
- bypassDoNotDisturb: true, // an alarm silenced by DND is not an alarm
114
- repeatAlert: { forMs: 15 * 60 * 1000 }, // required bound, 1–60 minutes
115
- timeSensitive: true,
116
- version: 1, // unchanged — these need no bump
117
- }]);
118
- ```
119
-
120
- Four things to get right, none of them visible in the signatures:
121
-
122
- 1. **The duration is mandatory and bounded (1–60 min, `REPEAT_ALERT_BOUNDS`).** There is no
123
- unbounded form; the type forbids it. Do not reach for a workaround — an alert with no
124
- bound means a user comes home to a phone still ringing.
125
- 2. **Expiry stops the sound; it never removes the notification.** The user must still be
126
- able to see what they missed. If you find yourself cancelling at expiry, you have
127
- inverted the requirement. The window is a **floor**, not a precise stop — the entry
128
- that ends the alert inherits normal delivery accuracy, so without exact alarms it
129
- overruns (a declared 2 min measured 2m35s–3m00s). Never promise a caller an exact cut-off.
130
- 3. **Repeating tones need one setup step**, at module scope in `index.js`, never in a
131
- component or an effect:
132
- ```js
133
- import { registerBackgroundNotificationHandler } from '@chipmobilesdk/rn-notification';
134
- registerBackgroundNotificationHandler({ engine });
135
- ```
136
- Without it nothing ends the alert while the app is dead. `apply()` reports
137
- `REPEAT_STOP_HANDLER_MISSING`. An app with no repeating tone needs none of this.
138
- 4. **iOS refuses repetition entirely** — no looping API exists, and the alert level that
139
- would need an Apple-granted entitlement does not loop either. It is reported in
140
- `result.repeatUnavailable`; the notification still delivers and sounds once. Never
141
- promise a caller an iOS alarm loops. `timeSensitive` on iOS also needs the consuming
142
- app's own Time Sensitive Notifications capability, or it degrades to `active`.
143
-
144
- Read `## Tones and channels` before promising behaviour to anyone: it carries the
145
- per-platform matrix and six limits that stay true (notification volume rather than alarm
146
- volume on Android, the shade stopping an insistent sound, vendor battery management).
147
-
148
- ## Changing a tone: when a version bump is required
149
-
150
- A bump means a **new channel**, and a new channel means the user loses every setting they
151
- adjusted. Never bump one to fix a label.
152
-
153
- | Change | Bump `version`? |
154
- |---|---|
155
- | `name`, `description` | **No** — updated in place on existing installs |
156
- | `repeatAlert`, `timeSensitive` | **No** — realized per notification |
157
- | `urgency`, `sound`, `vibration`, `badge`, `groupId`, `bypassDoNotDisturb` | **Yes** |
158
-
159
- `apply()` reports a label correction as `labelsRefreshed`, distinct from `created`,
160
- `migrated`, and `unchanged`.
161
-
162
- ## Debugging "it did not arrive"
163
-
164
- First check the installed version: **before 0.2.1** the state record never persisted on a
165
- device (it used react-native-mmkv's v3 constructor, absent in v4), so deduplication and
166
- cold-start tap buffering only lasted one session. Require `0.2.1` or later.
167
-
168
- Capture a state snapshot (`createSnapshot`) before guessing — permissions, applied channel
169
- versions, pending set, last maintenance cycle, discrepancies and state health, in one
170
- place. Some codes are **reported outcomes, not failures** (`TRUNCATED_OVER_CEILING`,
171
- `EXACT_ALARM_UNAVAILABLE`, `PENDING_SET_DISCREPANCY`, `BURST_THRESHOLD_EXCEEDED`,
172
- `PAYLOAD_VERSION_SKEW`); use `isNonFailure(code)` rather than treating every code as an
173
- error.
1
+ ---
2
+ name: sdk-rn-notification
3
+ description: "Local and push notifications via @chipmobilesdk/rn-notification: scheduling, identifiers, wall-clock vs absolute anchoring, idempotent reconciliation, recurrence windows, permissions, alarm-style repeating tones with a bounded window, tone labels in system settings, Android channel migration, tap routing, FCM. Use when scheduling a reminder or alarm, making a notification repeat until handled, naming a tone users see in settings, handling a notification tap, requesting notification permission, changing a channel, or debugging a notification that did not arrive."
4
+ metadata:
5
+ package: "@chipmobilesdk/rn-notification"
6
+ user-invocable: true
7
+ disable-model-invocation: false
8
+ ---
9
+
10
+ # @chipmobilesdk/rn-notification
11
+
12
+ Scheduled local notifications with stable identity and idempotent reconciliation,
13
+ provider-agnostic push, permissions, declarative channels and tones, and tap routing that
14
+ survives a cold start.
15
+
16
+ ## Use this when
17
+
18
+ - Scheduling, replacing, or cancelling a notification
19
+ - Reacting to a tap, an action button, or a dismissal
20
+ - Requesting notification or exact-alarm permission
21
+ - Declaring or changing a tone/channel, or enabling push
22
+ - Building an alarm-style reminder that repeats until the user handles it
23
+ - Debugging a notification that did not arrive, or arrived twice
24
+
25
+ **Not** for in-app banners or an inbox (this package renders nothing), for storing app
26
+ data (`@chipmobilesdk/rn-local-db`), or for translating text — resolve strings yourself.
27
+
28
+ ## Read before writing any code
29
+
30
+ Three things cause most of the bugs, and none is visible in the type signatures:
31
+
32
+ 1. **Identifiers are yours and must be derived from data.** `crypto.randomUUID()` means
33
+ you can never replace or cancel what you scheduled. Nothing is normalized, so
34
+ `Task:1` and `task:1` are two different notifications.
35
+ 2. **Wall-clock is the default anchoring mode** — an 08:00 reminder stays 08:00 after the
36
+ user changes time zone. One case re-anchoring cannot fix: read
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.
43
+
44
+ Read `## Choosing identifiers`, `## Choosing an anchoring mode`, and
45
+ `## Maintenance versus reconciliation` before designing anything.
46
+
47
+ **A refusal must never break the user's work.** Permission denial is a degradation, not a
48
+ failure: the caller's operation still succeeds and the lost capability is reported
49
+ separately. Without exact-alarm capability notifications are still scheduled, flagged
50
+ `mayBeDelayed`. Never gate a user action on a permission answer.
51
+
52
+ ## Android: two manifest entries you must add yourself
53
+
54
+ Consumer-owned; this package cannot ship either.
55
+
56
+ - **`SCHEDULE_EXACT_ALARM` arrives whether you want it or not** — the engine declares it
57
+ and manifest merging is transitive. It is store-restricted and needs a Play Console
58
+ declaration. Remove it with `tools:node="remove"` unless you need precise timing.
59
+ - **Backup exclusion** for the state record — a deduplication ledger restored onto another
60
+ device claims payloads were already delivered, silently suppressing real ones.
61
+
62
+ See `### Android` and `### Backup exclusion`.
63
+
64
+ ## Public API
65
+
66
+ Three entrypoints. The **root** exports one factory per concern (`createScheduler`,
67
+ `createReconciler`, `createRecurrence`, `createPermissions`, `createChannels`,
68
+ `createInteractions`, `createSnapshot`), the maintenance steps and their registry, plus
69
+ `resolveTone`, `registerBackgroundNotificationHandler`, `resetState`, `clearOwner`,
70
+ `isNonFailure`, `REPEAT_ALERT_BOUNDS` and `DIAGNOSTIC_CODES`.
71
+ **`/push`** carries the push gate, FCM provider, instruction handler and sign-out —
72
+ **import it only if you enable push**, because an app that never touches this subpath ships
73
+ no provider code, no push permissions, and no entitlements. **`/testing`** carries the fake
74
+ engine and provider, the conformance suite and the fault injector.
75
+
76
+ Enumerate exact names from `node_modules/@chipmobilesdk/rn-notification/src/index.ts`
77
+ rather than guessing.
78
+
79
+ ## Where the detail lives
80
+
81
+ `node_modules/@chipmobilesdk/rn-notification/README.md`:
82
+
83
+ | Need | Section |
84
+ |---|---|
85
+ | Install, peers, enabling push | `## Installation`, `### Enabling push (optional)` |
86
+ | Native setup | `### iOS`, `### Android`, `### Backup exclusion` |
87
+ | Naming notifications | `## Choosing identifiers` |
88
+ | Tones, alarms, labels, version bumps | `## Tones and channels` |
89
+ | Time zones and travel | `## Choosing an anchoring mode` |
90
+ | Automatic vs your responsibility | `## Maintenance versus reconciliation` |
91
+ | Converging the device to your data | `## Reconciliation`, `## Recurrence` |
92
+ | Permission timing and recovery | `## Permissions` |
93
+ | Taps, actions, cold start | `## Interaction` |
94
+ | Push and the backend contract | `## Push` |
95
+ | On-device storage and privacy | `## What is persisted`, `## Data behaviour …` |
96
+ | Limits and timing budgets | `## Budgets` |
97
+ | Error codes | `## Diagnostic identifiers` |
98
+
99
+ ## Alarm-style tones (0.2.0+)
100
+
101
+ For a reminder that must not be missed — medication, a wake-up, an appointment — declare
102
+ the repeat on the **tone**, not on each notification:
103
+
104
+ ```ts
105
+ await channels.apply([{
106
+ id: 'medication',
107
+ name: t('notifications.medication.name'), // localized; users read this in settings
108
+ description: t('notifications.medication.hint'),
109
+ urgency: 'max',
110
+ sound: { name: 'chime' },
111
+ vibration: true,
112
+ bypassDoNotDisturb: true, // an alarm silenced by DND is not an alarm
113
+ repeatAlert: { forMs: 15 * 60 * 1000 }, // required bound, 1–60 minutes
114
+ timeSensitive: true,
115
+ version: 1, // unchanged — these need no bump
116
+ }]);
117
+ ```
118
+
119
+ Four things to get right, none of them visible in the signatures:
120
+
121
+ 1. **The duration is mandatory and bounded (1–60 min, `REPEAT_ALERT_BOUNDS`).** There is no
122
+ unbounded form; the type forbids it. Do not reach for a workaround — an alert with no
123
+ bound means a user comes home to a phone still ringing.
124
+ 2. **Expiry stops the sound; it never removes the notification.** The user must still be
125
+ able to see what they missed. If you find yourself cancelling at expiry, you have
126
+ inverted the requirement. The window is a **floor**, not a precise stop — the entry
127
+ that ends the alert inherits normal delivery accuracy, so without exact alarms it
128
+ overruns (a declared 2 min measured 2m35s–3m00s). Never promise a caller an exact cut-off.
129
+ 3. **Repeating tones need one setup step**, at module scope in `index.js`, never in a
130
+ component or an effect:
131
+ ```js
132
+ import { registerBackgroundNotificationHandler } from '@chipmobilesdk/rn-notification';
133
+ registerBackgroundNotificationHandler({ engine });
134
+ ```
135
+ Without it nothing ends the alert while the app is dead. `apply()` reports
136
+ `REPEAT_STOP_HANDLER_MISSING`. An app with no repeating tone needs none of this.
137
+ 4. **iOS refuses repetition entirely** — no looping API exists, and the alert level that
138
+ would need an Apple-granted entitlement does not loop either. It is reported in
139
+ `result.repeatUnavailable`; the notification still delivers and sounds once. Never
140
+ promise a caller an iOS alarm loops. `timeSensitive` on iOS also needs the consuming
141
+ app's own Time Sensitive Notifications capability, or it degrades to `active`.
142
+
143
+ Read `## Tones and channels` before promising behaviour to anyone: it carries the
144
+ per-platform matrix and six limits that stay true (notification volume rather than alarm
145
+ volume on Android, the shade stopping an insistent sound, vendor battery management).
146
+
147
+ ## Changing a tone: when a version bump is required
148
+
149
+ A bump means a **new channel**, and a new channel means the user loses every setting they
150
+ adjusted. Never bump one to fix a label.
151
+
152
+ | Change | Bump `version`? |
153
+ |---|---|
154
+ | `name`, `description` | **No** — updated in place on existing installs |
155
+ | `repeatAlert`, `timeSensitive` | **No** — realized per notification |
156
+ | `urgency`, `sound`, `vibration`, `badge`, `groupId`, `bypassDoNotDisturb` | **Yes** |
157
+
158
+ `apply()` reports a label correction as `labelsRefreshed`, distinct from `created`,
159
+ `migrated`, and `unchanged`.
160
+
161
+ ## Debugging "it did not arrive"
162
+
163
+ First check the installed version: **before 0.2.1** the state record never persisted on a
164
+ device (it used react-native-mmkv's v3 constructor, absent in v4), so deduplication and
165
+ cold-start tap buffering only lasted one session. Require `0.2.1` or later.
166
+
167
+ Capture a state snapshot (`createSnapshot`) before guessing — permissions, applied channel
168
+ versions, pending set, last maintenance cycle, discrepancies and state health, in one
169
+ place. Some codes are **reported outcomes, not failures** (`TRUNCATED_OVER_CEILING`,
170
+ `EXACT_ALARM_UNAVAILABLE`, `PENDING_SET_DISCREPANCY`, `BURST_THRESHOLD_EXCEEDED`,
171
+ `PAYLOAD_VERSION_SKEW`); use `isNonFailure(code)` rather than treating every code as an
172
+ error.