@use-voltra/android 2.3.2 → 2.4.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 (74) hide show
  1. package/build/commonjs/index.js +25 -0
  2. package/build/commonjs/index.js.map +1 -1
  3. package/build/commonjs/jsx/primitives.js +36 -0
  4. package/build/commonjs/jsx/primitives.js.map +1 -1
  5. package/build/commonjs/modifiers/createAndroidModifier.js +20 -0
  6. package/build/commonjs/modifiers/createAndroidModifier.js.map +1 -0
  7. package/build/commonjs/modifiers/index.js +188 -0
  8. package/build/commonjs/modifiers/index.js.map +1 -0
  9. package/build/commonjs/ongoing-notification/components.js +7 -1
  10. package/build/commonjs/ongoing-notification/components.js.map +1 -1
  11. package/build/commonjs/ongoing-notification/renderer.js +354 -18
  12. package/build/commonjs/ongoing-notification/renderer.js.map +1 -1
  13. package/build/commonjs/ongoing-notification/types.js +56 -0
  14. package/build/commonjs/ongoing-notification/types.js.map +1 -1
  15. package/build/commonjs/server.js.map +1 -1
  16. package/build/module/index.js +3 -1
  17. package/build/module/index.js.map +1 -1
  18. package/build/module/jsx/primitives.js +1 -0
  19. package/build/module/jsx/primitives.js.map +1 -1
  20. package/build/module/modifiers/createAndroidModifier.js +15 -0
  21. package/build/module/modifiers/createAndroidModifier.js.map +1 -0
  22. package/build/module/modifiers/index.js +169 -0
  23. package/build/module/modifiers/index.js.map +1 -0
  24. package/build/module/ongoing-notification/components.js +6 -0
  25. package/build/module/ongoing-notification/components.js.map +1 -1
  26. package/build/module/ongoing-notification/renderer.js +354 -18
  27. package/build/module/ongoing-notification/renderer.js.map +1 -1
  28. package/build/module/ongoing-notification/types.js +55 -1
  29. package/build/module/ongoing-notification/types.js.map +1 -1
  30. package/build/module/server.js.map +1 -1
  31. package/build/typescript/commonjs/index.d.ts +4 -3
  32. package/build/typescript/commonjs/index.d.ts.map +1 -1
  33. package/build/typescript/commonjs/jsx/baseProps.d.ts +8 -0
  34. package/build/typescript/commonjs/jsx/baseProps.d.ts.map +1 -1
  35. package/build/typescript/commonjs/jsx/primitives.d.ts +1 -0
  36. package/build/typescript/commonjs/jsx/primitives.d.ts.map +1 -1
  37. package/build/typescript/commonjs/modifiers/createAndroidModifier.d.ts +13 -0
  38. package/build/typescript/commonjs/modifiers/createAndroidModifier.d.ts.map +1 -0
  39. package/build/typescript/commonjs/modifiers/index.d.ts +171 -0
  40. package/build/typescript/commonjs/modifiers/index.d.ts.map +1 -0
  41. package/build/typescript/commonjs/ongoing-notification/components.d.ts +8 -2
  42. package/build/typescript/commonjs/ongoing-notification/components.d.ts.map +1 -1
  43. package/build/typescript/commonjs/ongoing-notification/renderer.d.ts.map +1 -1
  44. package/build/typescript/commonjs/ongoing-notification/types.d.ts +281 -3
  45. package/build/typescript/commonjs/ongoing-notification/types.d.ts.map +1 -1
  46. package/build/typescript/commonjs/server.d.ts +1 -1
  47. package/build/typescript/commonjs/server.d.ts.map +1 -1
  48. package/build/typescript/module/index.d.ts +4 -3
  49. package/build/typescript/module/index.d.ts.map +1 -1
  50. package/build/typescript/module/jsx/baseProps.d.ts +8 -0
  51. package/build/typescript/module/jsx/baseProps.d.ts.map +1 -1
  52. package/build/typescript/module/jsx/primitives.d.ts +1 -0
  53. package/build/typescript/module/jsx/primitives.d.ts.map +1 -1
  54. package/build/typescript/module/modifiers/createAndroidModifier.d.ts +13 -0
  55. package/build/typescript/module/modifiers/createAndroidModifier.d.ts.map +1 -0
  56. package/build/typescript/module/modifiers/index.d.ts +171 -0
  57. package/build/typescript/module/modifiers/index.d.ts.map +1 -0
  58. package/build/typescript/module/ongoing-notification/components.d.ts +8 -2
  59. package/build/typescript/module/ongoing-notification/components.d.ts.map +1 -1
  60. package/build/typescript/module/ongoing-notification/renderer.d.ts.map +1 -1
  61. package/build/typescript/module/ongoing-notification/types.d.ts +281 -3
  62. package/build/typescript/module/ongoing-notification/types.d.ts.map +1 -1
  63. package/build/typescript/module/server.d.ts +1 -1
  64. package/build/typescript/module/server.d.ts.map +1 -1
  65. package/package.json +2 -2
  66. package/src/index.ts +36 -2
  67. package/src/jsx/baseProps.tsx +8 -0
  68. package/src/jsx/primitives.ts +1 -0
  69. package/src/modifiers/createAndroidModifier.ts +21 -0
  70. package/src/modifiers/index.ts +166 -0
  71. package/src/ongoing-notification/components.tsx +22 -1
  72. package/src/ongoing-notification/renderer.ts +451 -18
  73. package/src/ongoing-notification/types.ts +298 -4
  74. package/src/server.ts +15 -0
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+
3
+ import { createAndroidModifier } from './createAndroidModifier.js';
4
+
5
+ /**
6
+ * A static color string. Resolves to `never` for an `AndroidDynamicColors` token, so passing one
7
+ * where Glance needs a concrete color is a compile-time error.
8
+ */
9
+
10
+ /** At least one of the two keys is required. */
11
+
12
+ /**
13
+ * Adds padding in dp, mirrored in right-to-left layouts. The most specific edge wins (`start` over
14
+ * `horizontal` over `all`). Glance adds repeated padding together, including `style.padding`.
15
+ *
16
+ * @since Android 7.0
17
+ */
18
+ export const padding = values => {
19
+ if (typeof values === 'number') {
20
+ return createAndroidModifier('padding', {
21
+ all: values
22
+ });
23
+ }
24
+ // Copy only the known edges so extra keys on a caller's object never reach the device.
25
+ const {
26
+ all,
27
+ horizontal,
28
+ vertical,
29
+ start,
30
+ top,
31
+ end,
32
+ bottom
33
+ } = values;
34
+ return createAndroidModifier('padding', {
35
+ all,
36
+ horizontal,
37
+ vertical,
38
+ start,
39
+ top,
40
+ end,
41
+ bottom
42
+ });
43
+ };
44
+
45
+ /**
46
+ * Adds padding in dp that ignores the layout direction. Adds up with `padding` and `style.padding`.
47
+ *
48
+ * @since Android 7.0
49
+ */
50
+ export const absolutePadding = values => {
51
+ if (typeof values === 'number') {
52
+ return createAndroidModifier('absolutePadding', {
53
+ all: values
54
+ });
55
+ }
56
+ const {
57
+ all,
58
+ horizontal,
59
+ vertical,
60
+ left,
61
+ top,
62
+ right,
63
+ bottom
64
+ } = values;
65
+ return createAndroidModifier('absolutePadding', {
66
+ all,
67
+ horizontal,
68
+ vertical,
69
+ left,
70
+ top,
71
+ right,
72
+ bottom
73
+ });
74
+ };
75
+
76
+ /**
77
+ * Sets a fixed width in dp.
78
+ *
79
+ * @since Android 7.0
80
+ */
81
+ export const width = dp => createAndroidModifier('width', {
82
+ width: dp
83
+ });
84
+
85
+ /**
86
+ * Sets a fixed height in dp.
87
+ *
88
+ * @since Android 7.0
89
+ */
90
+ export const height = dp => createAndroidModifier('height', {
91
+ height: dp
92
+ });
93
+
94
+ /**
95
+ * Sets a fixed size in dp. A single number sets both dimensions.
96
+ *
97
+ * @since Android 7.0
98
+ */
99
+ export const size = value => createAndroidModifier('size', typeof value === 'number' ? {
100
+ width: value,
101
+ height: value
102
+ } : {
103
+ width: value.width,
104
+ height: value.height
105
+ });
106
+
107
+ /** @since Android 7.0 */
108
+ export const fillMaxWidth = () => createAndroidModifier('fillMaxWidth');
109
+ /** @since Android 7.0 */
110
+ export const fillMaxHeight = () => createAndroidModifier('fillMaxHeight');
111
+ /** @since Android 7.0 */
112
+ export const fillMaxSize = () => createAndroidModifier('fillMaxSize');
113
+ /** @since Android 7.0 */
114
+ export const wrapContentWidth = () => createAndroidModifier('wrapContentWidth');
115
+ /** @since Android 7.0 */
116
+ export const wrapContentHeight = () => createAndroidModifier('wrapContentHeight');
117
+ /** @since Android 7.0 */
118
+ export const wrapContentSize = () => createAndroidModifier('wrapContentSize');
119
+
120
+ /**
121
+ * Fills the background with a color: a static color, an `AndroidDynamicColors` token, or a pair of
122
+ * static colors for light and dark mode.
123
+ *
124
+ * @since Android 7.0
125
+ */
126
+ export const background = color => createAndroidModifier('background', typeof color === 'string' ? {
127
+ color
128
+ } : {
129
+ day: color.day,
130
+ night: color.night
131
+ });
132
+
133
+ /**
134
+ * Rounds the corners in dp. Glance ignores it below Android 12 (API 31).
135
+ *
136
+ * @since Android 12
137
+ */
138
+ export const cornerRadius = radius => createAndroidModifier('cornerRadius', {
139
+ radius
140
+ });
141
+
142
+ /**
143
+ * Shows, hides (keeping its space), or removes the component from layout.
144
+ *
145
+ * @since Android 7.0
146
+ */
147
+ export const visibility = value => createAndroidModifier('visibility', {
148
+ visibility: value
149
+ });
150
+
151
+ /**
152
+ * Sets accessibility and test metadata.
153
+ *
154
+ * @since Android 7.0
155
+ */
156
+ export const semantics = values => createAndroidModifier('semantics', {
157
+ contentDescription: values.contentDescription,
158
+ testTag: values.testTag
159
+ });
160
+
161
+ /**
162
+ * Marks the widget's background view so the launcher can animate it when the widget opens the app.
163
+ * Use it once per widget, on the outermost component. Only one component keeps it: the first that
164
+ * appears once in the widget, and none inside a widget with a `Scaffold`, which marks its own.
165
+ *
166
+ * @since Android 12
167
+ */
168
+ export const appWidgetBackground = () => createAndroidModifier('appWidgetBackground');
169
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["createAndroidModifier","padding","values","all","horizontal","vertical","start","top","end","bottom","absolutePadding","left","right","width","dp","height","size","value","fillMaxWidth","fillMaxHeight","fillMaxSize","wrapContentWidth","wrapContentHeight","wrapContentSize","background","color","day","night","cornerRadius","radius","visibility","semantics","contentDescription","testTag","appWidgetBackground"],"sourceRoot":"../../../src","sources":["modifiers/index.ts"],"mappings":"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","ignoreList":[]}
@@ -9,10 +9,16 @@ const createAndroidOngoingNotificationComponent = (displayName, kind) => {
9
9
  };
10
10
  export const Progress = createAndroidOngoingNotificationComponent('AndroidOngoingNotification.Progress', 'progress');
11
11
  export const BigText = createAndroidOngoingNotificationComponent('AndroidOngoingNotification.BigText', 'bigText');
12
+ export const BigPicture = createAndroidOngoingNotificationComponent('AndroidOngoingNotification.BigPicture', 'bigPicture');
13
+ export const Inbox = createAndroidOngoingNotificationComponent('AndroidOngoingNotification.Inbox', 'inbox');
14
+ export const Metric = createAndroidOngoingNotificationComponent('AndroidOngoingNotification.Metric', 'metric');
12
15
  export const Action = createAndroidOngoingNotificationComponent('AndroidOngoingNotification.Action', 'action');
13
16
  export const AndroidOngoingNotification = {
14
17
  Progress,
15
18
  BigText,
19
+ BigPicture,
20
+ Inbox,
21
+ Metric,
16
22
  Action
17
23
  };
18
24
  //# sourceMappingURL=components.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["ANDROID_ONGOING_NOTIFICATION_COMPONENT_TAG","Symbol","for","createAndroidOngoingNotificationComponent","displayName","kind","Component","_props","Progress","BigText","Action","AndroidOngoingNotification"],"sourceRoot":"../../../src","sources":["ongoing-notification/components.tsx"],"mappings":";;AAQA,OAAO,MAAMA,0CAA0C,GAAGC,MAAM,CAACC,GAAG,CAAC,+CAA+C,CAAC;AASrH,MAAMC,yCAAyC,GAAGA,CAChDC,WAAmB,EACnBC,IAA6C,KACG;EAChD,MAAMC,SAAS,GAAIC,MAAc,IAAK,IAAI;EAE1CD,SAAS,CAACF,WAAW,GAAGA,WAAW;EACjCE,SAAS,CAAiDN,0CAA0C,CAAC,GAAGK,IAAI;EAE9G,OAAOC,SAAS;AAClB,CAAC;AAED,OAAO,MAAME,QAAQ,GAAGL,yCAAyC,CAC/D,qCAAqC,EACrC,UACF,CAAC;AAED,OAAO,MAAMM,OAAO,GAAGN,yCAAyC,CAC9D,oCAAoC,EACpC,SACF,CAAC;AAED,OAAO,MAAMO,MAAM,GAAGP,yCAAyC,CAC7D,mCAAmC,EACnC,QACF,CAAC;AAED,OAAO,MAAMQ,0BAA0B,GAAG;EACxCH,QAAQ;EACRC,OAAO;EACPC;AACF,CAAU","ignoreList":[]}
1
+ {"version":3,"names":["ANDROID_ONGOING_NOTIFICATION_COMPONENT_TAG","Symbol","for","createAndroidOngoingNotificationComponent","displayName","kind","Component","_props","Progress","BigText","BigPicture","Inbox","Metric","Action","AndroidOngoingNotification"],"sourceRoot":"../../../src","sources":["ongoing-notification/components.tsx"],"mappings":";;AAWA,OAAO,MAAMA,0CAA0C,GAAGC,MAAM,CAACC,GAAG,CAAC,+CAA+C,CAAC;AASrH,MAAMC,yCAAyC,GAAGA,CAChDC,WAAmB,EACnBC,IAA6C,KACG;EAChD,MAAMC,SAAS,GAAIC,MAAc,IAAK,IAAI;EAE1CD,SAAS,CAACF,WAAW,GAAGA,WAAW;EACjCE,SAAS,CAAiDN,0CAA0C,CAAC,GAAGK,IAAI;EAE9G,OAAOC,SAAS;AAClB,CAAC;AAED,OAAO,MAAME,QAAQ,GAAGL,yCAAyC,CAC/D,qCAAqC,EACrC,UACF,CAAC;AAED,OAAO,MAAMM,OAAO,GAAGN,yCAAyC,CAC9D,oCAAoC,EACpC,SACF,CAAC;AAED,OAAO,MAAMO,UAAU,GAAGP,yCAAyC,CACjE,uCAAuC,EACvC,YACF,CAAC;AAED,OAAO,MAAMQ,KAAK,GAAGR,yCAAyC,CAC5D,kCAAkC,EAClC,OACF,CAAC;AAED,OAAO,MAAMS,MAAM,GAAGT,yCAAyC,CAC7D,mCAAmC,EACnC,QACF,CAAC;AAED,OAAO,MAAMU,MAAM,GAAGV,yCAAyC,CAC7D,mCAAmC,EACnC,QACF,CAAC;AAED,OAAO,MAAMW,0BAA0B,GAAG;EACxCN,QAAQ;EACRC,OAAO;EACPC,UAAU;EACVC,KAAK;EACLC,MAAM;EACNC;AACF,CAAU","ignoreList":[]}
@@ -5,6 +5,24 @@ import { getAndroidComponentId } from '../payload/component-ids.js';
5
5
  import { ANDROID_ONGOING_NOTIFICATION_COMPONENT_TAG } from './components.js';
6
6
  void getAndroidComponentId;
7
7
  const PAYLOAD_VERSION = 1;
8
+
9
+ /**
10
+ * InboxStyle only renders six lines, so more than that is rejected at render time instead of being
11
+ * silently truncated. Kept in sync with MAX_INBOX_LINES in the Android client.
12
+ */
13
+ const MAX_INBOX_LINES = 6;
14
+
15
+ /**
16
+ * Size of an inline picture that is still reasonable to hand to a native post. Push providers cap
17
+ * data payloads at a few kilobytes (FCM allows 4096 bytes), so no inline picture travels through a
18
+ * push message. Warns instead of failing: local starts and self-hosted transports can legitimately
19
+ * carry more. Compared against the decoded size, because a base64 string is four characters per
20
+ * three bytes and its length flatters the image.
21
+ */
22
+ const MAX_INLINE_PICTURE_BYTES = 256 * 1024;
23
+
24
+ /** Bytes a base64 string holds once decoded. Padding leaves this at most two bytes high. */
25
+ const decodedBase64Bytes = base64 => Math.ceil(base64.length * 3 / 4);
8
26
  const flattenChildren = node => {
9
27
  if (node === null || node === undefined || typeof node === 'boolean') {
10
28
  return [];
@@ -77,6 +95,13 @@ const assertOptionalImageSource = (value, propName) => {
77
95
  }
78
96
  return value;
79
97
  };
98
+ const assertRequiredImageSource = (value, propName) => {
99
+ const source = assertOptionalImageSource(value, propName);
100
+ if (source === undefined) {
101
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}" is required.`);
102
+ }
103
+ return source;
104
+ };
80
105
  const assertBoolean = (value, propName) => {
81
106
  if (value === undefined) {
82
107
  return undefined;
@@ -143,21 +168,91 @@ const normalizeProgressPoints = value => {
143
168
  };
144
169
  });
145
170
  };
146
- const normalizeWhen = value => {
147
- if (value === undefined) {
148
- return undefined;
171
+ const normalizeInboxLines = value => {
172
+ if (!Array.isArray(value)) {
173
+ throw new Error('[Voltra] [Android] Ongoing notification prop "lines" must be an array of non-empty strings.');
174
+ }
175
+ if (value.length === 0) {
176
+ throw new Error('[Voltra] [Android] Ongoing notification prop "lines" must contain at least one line.');
149
177
  }
178
+ if (value.length > MAX_INBOX_LINES) {
179
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "lines" must contain at most ${MAX_INBOX_LINES} lines, got ${value.length}.`);
180
+ }
181
+ return value.map((line, index) => assertString(line, `lines[${index}]`));
182
+ };
183
+ const warnInlinePicture = picture => {
184
+ const base64 = 'base64' in picture ? picture.base64 : undefined;
185
+ if (base64 === undefined) {
186
+ return;
187
+ }
188
+ const pictureBytes = decodedBase64Bytes(base64);
189
+ if (pictureBytes > MAX_INLINE_PICTURE_BYTES) {
190
+ console.warn(`[Voltra] [Android] Ongoing notification prop "picture" carries a ${pictureBytes}-byte image, which exceeds ${MAX_INLINE_PICTURE_BYTES} bytes. Inline pictures cannot travel through push messages: preload the image and reference it with assetName instead.`);
191
+ }
192
+ };
193
+ const normalizeEpoch = (value, propName) => {
150
194
  if (value instanceof Date) {
151
195
  const timestamp = value.getTime();
152
196
  if (!Number.isFinite(timestamp)) {
153
- throw new Error('[Voltra] [Android] Ongoing notification prop "when" must be a valid Date or timestamp.');
197
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}" must be a valid Date or timestamp.`);
154
198
  }
155
199
  return timestamp;
156
200
  }
157
201
  if (typeof value === 'number' && Number.isFinite(value)) {
158
202
  return value;
159
203
  }
160
- throw new Error('[Voltra] [Android] Ongoing notification prop "when" must be a valid Date or timestamp.');
204
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}" must be a valid Date or timestamp.`);
205
+ };
206
+ const normalizeWhen = value => {
207
+ if (value === undefined) {
208
+ return undefined;
209
+ }
210
+ return normalizeEpoch(value, 'when');
211
+ };
212
+
213
+ // `true` keeps meaning "count up" for payloads and props written before the
214
+ // chip gained a direction; only 'countDown' sets chronometerCountDown, and a
215
+ // countdown without `when` has nothing to count down to.
216
+ const normalizeChronometer = (value, when) => {
217
+ if (value === undefined) {
218
+ return {};
219
+ }
220
+ if (typeof value === 'boolean') {
221
+ return {
222
+ chronometer: value
223
+ };
224
+ }
225
+ if (value === 'countUp') {
226
+ return {
227
+ chronometer: true
228
+ };
229
+ }
230
+ if (value === 'countDown') {
231
+ if (when === undefined) {
232
+ throw new Error('[Voltra] [Android] Ongoing notification prop "chronometer" set to "countDown" requires the "when" prop.');
233
+ }
234
+ return {
235
+ chronometer: true,
236
+ chronometerCountDown: true
237
+ };
238
+ }
239
+ throw new Error('[Voltra] [Android] Ongoing notification prop "chronometer" must be a boolean, "countUp" or "countDown".');
240
+ };
241
+ const normalizePublicVersion = value => {
242
+ if (value === undefined) {
243
+ return undefined;
244
+ }
245
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
246
+ throw new Error('[Voltra] [Android] Ongoing notification prop "publicVersion" must be an object with a title.');
247
+ }
248
+ const {
249
+ title,
250
+ text
251
+ } = value;
252
+ return {
253
+ title: assertString(title, 'publicVersion.title'),
254
+ text: assertOptionalString(text, 'publicVersion.text')
255
+ };
161
256
  };
162
257
  const getElementKind = element => {
163
258
  const elementType = element.type;
@@ -170,6 +265,19 @@ const normalizeActionPayload = props => {
170
265
  icon: assertOptionalImageSource(props.icon, 'icon')
171
266
  };
172
267
  };
268
+ const normalizeCommonDisplayFields = props => {
269
+ const when = normalizeWhen(props.when);
270
+ return {
271
+ title: assertOptionalNonEmptyString(props.title, 'title'),
272
+ subText: assertOptionalString(props.subText, 'subText'),
273
+ shortCriticalText: assertOptionalString(props.shortCriticalText, 'shortCriticalText'),
274
+ when,
275
+ ...normalizeChronometer(props.chronometer, when),
276
+ showWhen: assertBoolean(props.showWhen, 'showWhen'),
277
+ largeIcon: assertOptionalImageSource(props.largeIcon, 'largeIcon'),
278
+ publicVersion: normalizePublicVersion(props.publicVersion)
279
+ };
280
+ };
173
281
  const normalizeActions = value => {
174
282
  const actions = [];
175
283
  Children.forEach(value, child => {
@@ -202,16 +310,11 @@ const normalizeProgressPayload = props => {
202
310
  return {
203
311
  v: PAYLOAD_VERSION,
204
312
  kind: 'progress',
205
- title: assertOptionalNonEmptyString(props.title, 'title'),
206
- subText: assertOptionalString(props.subText, 'subText'),
313
+ ...normalizeCommonDisplayFields(props),
207
314
  text: assertOptionalString(props.text, 'text'),
208
315
  value,
209
316
  max,
210
317
  indeterminate: assertBoolean(props.indeterminate, 'indeterminate'),
211
- shortCriticalText: assertOptionalString(props.shortCriticalText, 'shortCriticalText'),
212
- when: normalizeWhen(props.when),
213
- chronometer: assertBoolean(props.chronometer, 'chronometer'),
214
- largeIcon: assertOptionalImageSource(props.largeIcon, 'largeIcon'),
215
318
  progressTrackerIcon: assertOptionalImageSource(props.progressTrackerIcon, 'progressTrackerIcon'),
216
319
  progressStartIcon: assertOptionalImageSource(props.progressStartIcon, 'progressStartIcon'),
217
320
  progressEndIcon: assertOptionalImageSource(props.progressEndIcon, 'progressEndIcon'),
@@ -225,14 +328,238 @@ const normalizeBigTextPayload = props => {
225
328
  return {
226
329
  v: PAYLOAD_VERSION,
227
330
  kind: 'bigText',
228
- title: assertOptionalNonEmptyString(props.title, 'title'),
229
- subText: assertOptionalString(props.subText, 'subText'),
331
+ ...normalizeCommonDisplayFields(props),
230
332
  text,
231
333
  bigText: assertOptionalString(props.bigText, 'bigText') ?? text,
232
- shortCriticalText: assertOptionalString(props.shortCriticalText, 'shortCriticalText'),
233
- when: normalizeWhen(props.when),
234
- chronometer: assertBoolean(props.chronometer, 'chronometer'),
235
- largeIcon: assertOptionalImageSource(props.largeIcon, 'largeIcon'),
334
+ actions: normalizeActions(props.children)
335
+ };
336
+ };
337
+ const METRIC_TIME_PATTERN = /^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/;
338
+ const METRIC_SEMANTIC_STYLES = ['unspecified', 'info', 'safe', 'caution', 'danger'];
339
+ const METRIC_TIME_FORMATS = ['adaptive', 'chronometer'];
340
+ const assertMetricNonEmptyString = (value, propName) => {
341
+ if (typeof value !== 'string' || value.length === 0) {
342
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}" must be a non-empty string.`);
343
+ }
344
+ return value;
345
+ };
346
+ const normalizeMetricTimeFormat = (value, propName) => {
347
+ if (value === undefined) {
348
+ return undefined;
349
+ }
350
+ if (!METRIC_TIME_FORMATS.includes(value)) {
351
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}" must be "adaptive" or "chronometer".`);
352
+ }
353
+ return value;
354
+ };
355
+ const normalizeMetricValue = (value, unit, propName) => {
356
+ const topLevelUnit = assertOptionalString(unit, `${propName}.unit`);
357
+ const rejectTopLevelUnit = () => {
358
+ if (topLevelUnit !== undefined) {
359
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.unit" only applies to "int", "float" and "text" values.`);
360
+ }
361
+ };
362
+
363
+ // Shorthands: an integer reads as int, any other number as float, a string as text.
364
+ if (typeof value === 'number') {
365
+ if (!Number.isFinite(value)) {
366
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.value" must be a finite number.`);
367
+ }
368
+ return Number.isInteger(value) ? {
369
+ type: 'int',
370
+ value,
371
+ unit: topLevelUnit
372
+ } : {
373
+ type: 'float',
374
+ value,
375
+ unit: topLevelUnit
376
+ };
377
+ }
378
+ if (typeof value === 'string') {
379
+ return {
380
+ type: 'text',
381
+ value: assertMetricNonEmptyString(value, `${propName}.value`),
382
+ unit: topLevelUnit
383
+ };
384
+ }
385
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
386
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.value" must be a number, a string, or a metric value object.`);
387
+ }
388
+ const source = value;
389
+ const type = source.type;
390
+ switch (type) {
391
+ case 'int':
392
+ {
393
+ const numberValue = assertFiniteNumber(source.value, `${propName}.value`);
394
+ if (!Number.isInteger(numberValue)) {
395
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.value" must be an integer.`);
396
+ }
397
+ return {
398
+ type: 'int',
399
+ value: numberValue,
400
+ unit: assertOptionalString(source.unit, `${propName}.unit`) ?? topLevelUnit
401
+ };
402
+ }
403
+ case 'float':
404
+ {
405
+ const numberValue = assertFiniteNumber(source.value, `${propName}.value`);
406
+ const min = source.min === undefined ? undefined : assertFiniteNumber(source.min, `${propName}.min`);
407
+ const max = source.max === undefined ? undefined : assertFiniteNumber(source.max, `${propName}.max`);
408
+ if (min !== undefined && min < 0) {
409
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.min" must be greater than or equal to 0.`);
410
+ }
411
+ if (max !== undefined && max < 0) {
412
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.max" must be greater than or equal to 0.`);
413
+ }
414
+ if (min !== undefined && max !== undefined && min > max) {
415
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.min" must not exceed "${propName}.max".`);
416
+ }
417
+ let fractionDigits;
418
+ if (source.fractionDigits !== undefined) {
419
+ fractionDigits = assertFiniteNumber(source.fractionDigits, `${propName}.fractionDigits`);
420
+ if (!Number.isInteger(fractionDigits) || fractionDigits < 0) {
421
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.fractionDigits" must be an integer greater than or equal to 0.`);
422
+ }
423
+ }
424
+ return {
425
+ type: 'float',
426
+ value: numberValue,
427
+ unit: assertOptionalString(source.unit, `${propName}.unit`) ?? topLevelUnit,
428
+ min,
429
+ max,
430
+ fractionDigits
431
+ };
432
+ }
433
+ case 'text':
434
+ return {
435
+ type: 'text',
436
+ value: assertMetricNonEmptyString(source.value, `${propName}.value`),
437
+ unit: assertOptionalString(source.unit, `${propName}.unit`) ?? topLevelUnit
438
+ };
439
+ case 'time':
440
+ {
441
+ rejectTopLevelUnit();
442
+ const timeValue = assertMetricNonEmptyString(source.value, `${propName}.value`);
443
+ if (!METRIC_TIME_PATTERN.test(timeValue)) {
444
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.value" must be a time in "HH:mm" or "HH:mm:ss" form.`);
445
+ }
446
+ return {
447
+ type: 'time',
448
+ value: timeValue
449
+ };
450
+ }
451
+ case 'timer':
452
+ rejectTopLevelUnit();
453
+ return {
454
+ type: 'timer',
455
+ endsAt: normalizeEpoch(source.endsAt, `${propName}.endsAt`),
456
+ format: normalizeMetricTimeFormat(source.format, `${propName}.format`)
457
+ };
458
+ case 'stopwatch':
459
+ rejectTopLevelUnit();
460
+ return {
461
+ type: 'stopwatch',
462
+ startedAt: normalizeEpoch(source.startedAt, `${propName}.startedAt`),
463
+ format: normalizeMetricTimeFormat(source.format, `${propName}.format`)
464
+ };
465
+ case 'pausedTimer':
466
+ {
467
+ rejectTopLevelUnit();
468
+ const remainingMillis = assertFiniteNumber(source.remainingMillis, `${propName}.remainingMillis`);
469
+ if (remainingMillis < 0) {
470
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.remainingMillis" must be greater than or equal to 0.`);
471
+ }
472
+ return {
473
+ type: 'pausedTimer',
474
+ remainingMillis
475
+ };
476
+ }
477
+ case 'pausedStopwatch':
478
+ {
479
+ rejectTopLevelUnit();
480
+ const elapsedMillis = assertFiniteNumber(source.elapsedMillis, `${propName}.elapsedMillis`);
481
+ if (elapsedMillis < 0) {
482
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.elapsedMillis" must be greater than or equal to 0.`);
483
+ }
484
+ return {
485
+ type: 'pausedStopwatch',
486
+ elapsedMillis
487
+ };
488
+ }
489
+ default:
490
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "${propName}.type" must be "int", "float", "text", "time", "timer", "stopwatch", "pausedTimer", or "pausedStopwatch".`);
491
+ }
492
+ };
493
+ const normalizeMetricPayload = props => {
494
+ if (!Array.isArray(props.metrics)) {
495
+ throw new Error('[Voltra] [Android] Ongoing notification prop "metrics" must be an array.');
496
+ }
497
+ if (props.metrics.length < 1 || props.metrics.length > 3) {
498
+ throw new Error('[Voltra] [Android] Ongoing notification prop "metrics" must contain between 1 and 3 metrics.');
499
+ }
500
+ const metrics = props.metrics.map((descriptor, index) => {
501
+ if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) {
502
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "metrics[${index}]" must be an object.`);
503
+ }
504
+ const label = assertMetricNonEmptyString(descriptor.label, `metrics[${index}].label`);
505
+ if (label.length > 10) {
506
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "metrics[${index}].label" must be at most 10 characters long.`);
507
+ }
508
+ return {
509
+ label,
510
+ value: normalizeMetricValue(descriptor.value, descriptor.unit, `metrics[${index}]`)
511
+ };
512
+ });
513
+ let criticalMetric;
514
+ if (props.criticalMetric !== undefined) {
515
+ criticalMetric = assertFiniteNumber(props.criticalMetric, 'criticalMetric');
516
+ if (!Number.isInteger(criticalMetric) || criticalMetric < 0 || criticalMetric >= metrics.length) {
517
+ throw new Error(`[Voltra] [Android] Ongoing notification prop "criticalMetric" must be an index between 0 and ${metrics.length - 1}.`);
518
+ }
519
+ }
520
+ let semanticStyle;
521
+ if (props.semanticStyle !== undefined) {
522
+ if (!METRIC_SEMANTIC_STYLES.includes(props.semanticStyle)) {
523
+ throw new Error('[Voltra] [Android] Ongoing notification prop "semanticStyle" must be "unspecified", "info", "safe", "caution", or "danger".');
524
+ }
525
+ semanticStyle = props.semanticStyle;
526
+ }
527
+ return {
528
+ v: PAYLOAD_VERSION,
529
+ kind: 'metric',
530
+ ...normalizeCommonDisplayFields(props),
531
+ metrics,
532
+ criticalMetric,
533
+ semanticStyle,
534
+ actions: normalizeActions(props.children)
535
+ };
536
+ };
537
+ const normalizeBigPicturePayload = props => {
538
+ const picture = assertRequiredImageSource(props.picture, 'picture');
539
+ warnInlinePicture(picture);
540
+ return {
541
+ v: PAYLOAD_VERSION,
542
+ kind: 'bigPicture',
543
+ ...normalizeCommonDisplayFields(props),
544
+ text: assertOptionalString(props.text, 'text'),
545
+ picture,
546
+ summaryText: assertOptionalNonEmptyString(props.summaryText, 'summaryText'),
547
+ pictureContentDescription: assertOptionalNonEmptyString(props.pictureContentDescription, 'pictureContentDescription'),
548
+ showPictureWhenCollapsed: assertBoolean(props.showPictureWhenCollapsed, 'showPictureWhenCollapsed'),
549
+ bigLargeIcon: assertOptionalImageSource(props.bigLargeIcon, 'bigLargeIcon'),
550
+ hideLargeIconWhenExpanded: assertBoolean(props.hideLargeIconWhenExpanded, 'hideLargeIconWhenExpanded'),
551
+ actions: normalizeActions(props.children)
552
+ };
553
+ };
554
+ const normalizeInboxPayload = props => {
555
+ const lines = normalizeInboxLines(props.lines);
556
+ return {
557
+ v: PAYLOAD_VERSION,
558
+ kind: 'inbox',
559
+ ...normalizeCommonDisplayFields(props),
560
+ text: props.text === undefined ? lines[0] : assertString(props.text, 'text'),
561
+ lines,
562
+ summaryText: assertOptionalNonEmptyString(props.summaryText, 'summaryText'),
236
563
  actions: normalizeActions(props.children)
237
564
  };
238
565
  };
@@ -245,7 +572,16 @@ export const renderAndroidOngoingNotificationPayloadToJson = content => {
245
572
  if (kind === 'bigText') {
246
573
  return normalizeBigTextPayload(element.props);
247
574
  }
248
- throw new Error('[Voltra] [Android] Ongoing notification content must use AndroidOngoingNotification.Progress or AndroidOngoingNotification.BigText.');
575
+ if (kind === 'bigPicture') {
576
+ return normalizeBigPicturePayload(element.props);
577
+ }
578
+ if (kind === 'inbox') {
579
+ return normalizeInboxPayload(element.props);
580
+ }
581
+ if (kind === 'metric') {
582
+ return normalizeMetricPayload(element.props);
583
+ }
584
+ throw new Error('[Voltra] [Android] Ongoing notification content must use AndroidOngoingNotification.Progress, AndroidOngoingNotification.BigText, AndroidOngoingNotification.BigPicture, AndroidOngoingNotification.Inbox, or AndroidOngoingNotification.Metric.');
249
585
  };
250
586
  export const renderAndroidOngoingNotificationPayload = content => {
251
587
  return JSON.stringify(renderAndroidOngoingNotificationPayloadToJson(content));