@onekeyfe/react-native-native-list 3.0.105 → 3.0.107

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/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +16 -3
  2. package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +6 -0
  3. package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +671 -52
  4. package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +282 -54
  5. package/ios/HybridNativeList.swift +8 -2
  6. package/ios/NativeListCell.swift +630 -31
  7. package/ios/NativeListDesignAssets.swift +24 -1
  8. package/ios/RNCNativeListView.swift +216 -42
  9. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/Contents.json +15 -0
  10. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_account_error_custom.imageset/icon.svg +14 -0
  11. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/Contents.json +15 -0
  12. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_all_networks_solid.imageset/icon.svg +12 -0
  13. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/Contents.json +15 -0
  14. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_apple_brand.imageset/icon.svg +6 -0
  15. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/Contents.json +15 -0
  16. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_bot_illus.imageset/icon.svg +15 -0
  17. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/Contents.json +15 -0
  18. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_crossed_small_solid.imageset/icon.svg +3 -0
  19. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/Contents.json +15 -0
  20. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_globus_outline.imageset/icon.svg +7 -0
  21. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/Contents.json +15 -0
  22. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_google_illus.imageset/icon.svg +18 -0
  23. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/Contents.json +15 -0
  24. package/ios/Resources/NativeListIcons.xcassets/onekey_selector_lock_solid.imageset/icon.svg +7 -0
  25. package/lib/module/NativeList.js +102 -8
  26. package/lib/module/NativeList.web.js +20 -2
  27. package/lib/module/avatarPrefetch.js +174 -0
  28. package/lib/module/validation.js +45 -2
  29. package/lib/module/web/NativeListAvatarWorker.js +542 -0
  30. package/lib/module/web/NativeListWebAvatarCache.js +267 -0
  31. package/lib/module/web/NativeListWebEngine.js +958 -47
  32. package/lib/typescript/src/NativeList.web.d.ts +2 -2
  33. package/lib/typescript/src/avatarPrefetch.d.ts +28 -0
  34. package/lib/typescript/src/models.d.ts +72 -4
  35. package/lib/typescript/src/web/NativeListWebAvatarCache.d.ts +6 -0
  36. package/lib/typescript/src/web/NativeListWebEngine.d.ts +10 -1
  37. package/package.json +2 -2
  38. package/src/NativeList.tsx +149 -13
  39. package/src/NativeList.web.tsx +30 -3
  40. package/src/avatarPrefetch.ts +236 -0
  41. package/src/models.ts +98 -7
  42. package/src/validation.ts +131 -2
  43. package/src/web/NativeListAvatarWorker.js +567 -0
  44. package/src/web/NativeListWebAvatarCache.ts +314 -0
  45. package/src/web/NativeListWebEngine.ts +1409 -60
@@ -0,0 +1,236 @@
1
+ // OneKey patch: share a bounded URI-only window across renderers. No bitmap or
2
+ // account-specific data enters list snapshots or the UI runtime.
3
+ import type { ImageSource, LeadingVisual, RowModel, RowPatch } from './models';
4
+
5
+ export type AvatarCandidate = Readonly<{
6
+ source: ImageSource;
7
+ priority: 0 | 1 | 2;
8
+ }>;
9
+ const PREFIX = 'onekey-avatar://blockie/v1/';
10
+ const MAX_CANDIDATES = 64;
11
+ const IMAGE_PATCH_FIELDS = [
12
+ 'leading',
13
+ 'secondaryLeading',
14
+ 'image',
15
+ 'networkImage',
16
+ 'thumbnail',
17
+ 'visual',
18
+ ] as const;
19
+
20
+ export function avatarPrefetchWindow(
21
+ rows: readonly RowModel[],
22
+ first: number,
23
+ last: number,
24
+ direction: number,
25
+ resolveRow?: (row: RowModel) => RowModel
26
+ ): AvatarCandidate[] {
27
+ if (first < 0 || last < first || first >= rows.length) return [];
28
+ last = Math.min(last, rows.length - 1);
29
+ const result: AvatarCandidate[] = [];
30
+ const seen = new Set<string>();
31
+ let limit = MAX_CANDIDATES;
32
+ const add = (source: ImageSource | undefined, priority: 0 | 1 | 2) => {
33
+ if (
34
+ !source?.uri.startsWith(PREFIX) ||
35
+ source.cachePolicy === 'none' ||
36
+ seen.has(source.uri) ||
37
+ result.length >= limit
38
+ )
39
+ return;
40
+ seen.add(source.uri);
41
+ result.push({ source, priority });
42
+ };
43
+ const visual = (value: LeadingVisual | undefined, priority: 0 | 1 | 2) => {
44
+ if (!value) return;
45
+ if ('image' in value) add(value.image, priority);
46
+ if ('networkImage' in value) add(value.networkImage, priority);
47
+ if ('images' in value)
48
+ value.images.forEach((image) => add(image, priority));
49
+ if ('overlays' in value)
50
+ value.overlays?.forEach((overlay) => add(overlay.image, priority));
51
+ };
52
+ const row = (value: RowModel, priority: 0 | 1 | 2) => {
53
+ value = resolveRow?.(value) ?? value;
54
+ if ('leading' in value) visual(value.leading, priority);
55
+ if ('secondaryLeading' in value) visual(value.secondaryLeading, priority);
56
+ if ('image' in value) add(value.image, priority);
57
+ if ('networkImage' in value) add(value.networkImage, priority);
58
+ if ('thumbnail' in value) add(value.thumbnail, priority);
59
+ if (value.type === 'walletGroup') {
60
+ visual(value.parent.leading, priority);
61
+ for (const child of value.children.slice(0, MAX_CANDIDATES)) {
62
+ if (result.length >= limit) break;
63
+ visual(child.leading, priority);
64
+ }
65
+ }
66
+ };
67
+ // Bound row examination too: a giant non-avatar group must not scan all data.
68
+ for (let index = first; index <= last && index < first + 64; index += 1)
69
+ row(rows[index], 0);
70
+ const span = Math.min(24, (last - first + 1) * 2);
71
+ const step = direction < 0 ? -1 : 1;
72
+ const aheadStart = step > 0 ? last + 1 : first - 1;
73
+ const aheadLimit = Math.min(MAX_CANDIDATES, result.length + 24);
74
+ limit = aheadLimit;
75
+ for (
76
+ let distance = 0;
77
+ distance < span && result.length < aheadLimit;
78
+ distance += 1
79
+ ) {
80
+ const index = aheadStart + distance * step;
81
+ if (index < 0 || index >= rows.length) break;
82
+ row(rows[index], 1);
83
+ }
84
+ const behindStart = step > 0 ? first - 1 : last + 1;
85
+ const behindLimit = Math.min(MAX_CANDIDATES, result.length + 8);
86
+ limit = behindLimit;
87
+ for (
88
+ let distance = 0;
89
+ distance < Math.min(8, span) && result.length < behindLimit;
90
+ distance += 1
91
+ ) {
92
+ const index = behindStart - distance * step;
93
+ if (index < 0 || index >= rows.length) break;
94
+ row(rows[index], 2);
95
+ }
96
+ return result;
97
+ }
98
+
99
+ // OneKey patch: imperative image patches update a sparse prefetch overlay, not
100
+ // the readonly prop or a full cloned snapshot. The key index is rebuilt only for
101
+ // a complete snapshot; ordinary balance patches retain no extra row copies.
102
+ export class NativeAvatarPrefetchModel {
103
+ private rows: readonly RowModel[];
104
+ private rowByKey: Map<string, RowModel>;
105
+ private readonly imageRows = new Map<string, RowModel>();
106
+
107
+ constructor(rows: readonly RowModel[]) {
108
+ this.rows = rows;
109
+ this.rowByKey = new Map(rows.map((row) => [row.key, row]));
110
+ }
111
+
112
+ replaceSnapshot(rows: readonly RowModel[]) {
113
+ this.rows = rows;
114
+ this.rowByKey = new Map(rows.map((row) => [row.key, row]));
115
+ this.imageRows.clear();
116
+ }
117
+
118
+ applyPatches(patches: readonly RowPatch[], dispatch?: () => void): boolean {
119
+ // Native methods have no acknowledgement. Mirror only dispatched batches;
120
+ // a missing host or a synchronous serialization/dispatch failure changes nothing.
121
+ if (!dispatch) return false;
122
+ dispatch();
123
+ const seen = new Set<string>();
124
+ for (const patch of patches) {
125
+ const row = this.rowByKey.get(patch.key);
126
+ // Both native renderers reject the whole batch for an unknown key/type.
127
+ if (!row || row.type !== patch.type || seen.has(patch.key)) return false;
128
+ seen.add(patch.key);
129
+ }
130
+ let changed = false;
131
+ for (const patch of patches) {
132
+ const changes: Readonly<Record<string, unknown>> = patch.changes;
133
+ let imageChanges: Record<string, unknown> | undefined;
134
+ for (const key of IMAGE_PATCH_FIELDS) {
135
+ if (changes[key] !== undefined) {
136
+ imageChanges ??= {};
137
+ imageChanges[key] = changes[key];
138
+ }
139
+ }
140
+ // JSON.stringify omits undefined top-level fields. Native therefore keeps
141
+ // them unchanged; an explicit replacement visual without image clears it.
142
+ if (!imageChanges) continue;
143
+ const row =
144
+ this.imageRows.get(patch.key) ?? this.rowByKey.get(patch.key)!;
145
+ // Preserve the validated row discriminant, matching applyRowPatches' shallow merge.
146
+ this.imageRows.set(patch.key, {
147
+ ...row,
148
+ ...imageChanges,
149
+ key: row.key,
150
+ type: row.type,
151
+ } as RowModel);
152
+ changed = true;
153
+ }
154
+ return changed;
155
+ }
156
+
157
+ window(first: number, last: number, direction: number): AvatarCandidate[] {
158
+ return avatarPrefetchWindow(
159
+ this.rows,
160
+ first,
161
+ last,
162
+ direction,
163
+ (row) => this.imageRows.get(row.key) ?? row
164
+ );
165
+ }
166
+ }
167
+
168
+ // Native preload has no cancellation token. One in-flight source is the bound;
169
+ // direction changes replace all not-yet-started work rather than appending it.
170
+ export class NativeAvatarPrefetchQueue {
171
+ private pending: ImageSource[] = [];
172
+ private activeUri: string | undefined;
173
+ private timer: ReturnType<typeof setTimeout> | undefined;
174
+ private disposed = false;
175
+ private readonly recent = new Map<string, number>();
176
+
177
+ constructor(
178
+ private readonly preload: (source: ImageSource) => Promise<boolean>
179
+ ) {}
180
+
181
+ update(candidates: readonly AvatarCandidate[]) {
182
+ if (this.disposed) return;
183
+ const now = Date.now();
184
+ this.pending = candidates
185
+ .filter(
186
+ ({ source, priority }) =>
187
+ priority !== 0 &&
188
+ source.uri !== this.activeUri &&
189
+ now - (this.recent.get(source.uri) ?? 0) > 30_000
190
+ )
191
+ .map(({ source }) => source);
192
+ this.schedule();
193
+ }
194
+
195
+ dispose() {
196
+ this.disposed = true;
197
+ this.pending = [];
198
+ this.recent.clear();
199
+ if (this.timer !== undefined) clearTimeout(this.timer);
200
+ this.timer = undefined;
201
+ }
202
+
203
+ private schedule() {
204
+ if (
205
+ this.disposed ||
206
+ this.activeUri ||
207
+ this.timer !== undefined ||
208
+ !this.pending.length
209
+ )
210
+ return;
211
+ // Yield between short batches without imposing a frame-per-image throughput cap.
212
+ this.timer = setTimeout(() => {
213
+ this.timer = undefined;
214
+ const source = this.pending.shift();
215
+ if (!source || this.disposed) return;
216
+ this.activeUri = source.uri;
217
+ Promise.resolve()
218
+ .then(() => this.preload(source))
219
+ .then((success) => {
220
+ if (success && !this.disposed) {
221
+ this.recent.delete(source.uri);
222
+ this.recent.set(source.uri, Date.now());
223
+ if (this.recent.size > 128)
224
+ this.recent.delete(this.recent.keys().next().value as string);
225
+ }
226
+ })
227
+ .catch(() => {
228
+ /* Visible loading retains its own failure/retry behavior. */
229
+ })
230
+ .finally(() => {
231
+ this.activeUri = undefined;
232
+ this.schedule();
233
+ });
234
+ }, 0);
235
+ }
236
+ }
package/src/models.ts CHANGED
@@ -22,6 +22,9 @@ export type ImageSource = Readonly<{
22
22
  optimizeTos?: boolean;
23
23
  overscan?: number;
24
24
  loadingStrategy?: ImageLoadingStrategy;
25
+ // OneKey patch: fallbackUri is Web-only; retryTimes opts all platforms into terminal retries.
26
+ fallbackUri?: string;
27
+ retryTimes?: number;
25
28
  }>;
26
29
 
27
30
  export type BadgeModel = Readonly<{
@@ -30,7 +33,34 @@ export type BadgeModel = Readonly<{
30
33
  tone?: 'neutral' | 'info' | 'success' | 'warning' | 'danger';
31
34
  }>;
32
35
 
36
+ // OneKey patch: keep selector decoration and text data serializable.
37
+ export type SelectorTextSegment = Readonly<{
38
+ text: string;
39
+ textSegments?: readonly ValueTextSegment[];
40
+ tone?: TextTone | 'disabled' | 'caution';
41
+ separatorBefore?: boolean;
42
+ }>;
43
+ export type VisualOverlay = Readonly<{
44
+ position: 'topLeft' | 'bottomRight';
45
+ size?: number;
46
+ width?: number;
47
+ height?: number;
48
+ padding?: number;
49
+ offset?: number;
50
+ offsetX?: number;
51
+ offsetY?: number;
52
+ image?: ImageSource;
53
+ name?: string;
54
+ text?: string;
55
+ tintColor?: string;
56
+ backgroundColor?: string;
57
+ }>;
58
+
33
59
  type VisualWithImage = Readonly<{
60
+ fallbackIcon?: Readonly<{ name: string; tintColor?: string }>;
61
+ overlays?: readonly VisualOverlay[];
62
+ borderStyle?: 'dashed';
63
+ borderColor?: string;
34
64
  image?: ImageSource;
35
65
  fallbackText?: string;
36
66
  backgroundColor?: string;
@@ -70,8 +100,16 @@ export type SelectionTarget =
70
100
  | Readonly<{ scope: 'section'; sectionKey: string }>
71
101
  | Readonly<{ scope: 'list' }>;
72
102
 
103
+ // OneKey patch: preserve compact very-small-balance digit runs.
104
+ export type ValueTextSegment = Readonly<{ text: string; style?: 'subscript' }>;
105
+
73
106
  export type TrailingAccessory =
74
- | Readonly<{ kind: 'value'; text: string; secondary?: boolean }>
107
+ | Readonly<{
108
+ kind: 'value';
109
+ text: string;
110
+ secondary?: boolean;
111
+ textSegments?: readonly ValueTextSegment[];
112
+ }>
75
113
  | Readonly<{
76
114
  kind: 'valuePair';
77
115
  primary: string;
@@ -108,6 +146,9 @@ export type TrailingAccessory =
108
146
  tintColor?: string;
109
147
  disabled?: boolean;
110
148
  actionKey?: string;
149
+ testID?: string;
150
+ hoverActionKey?: string;
151
+ accessibilityLabel?: string;
111
152
  }>
112
153
  | Readonly<{ kind: 'spinner' }>
113
154
  | Readonly<{ kind: 'progress'; value: number }>;
@@ -120,6 +161,15 @@ export type FooterAction = Readonly<{
120
161
  }>;
121
162
 
122
163
  export type RowBase = Readonly<{
164
+ // OneKey patch: preserve measured selector geometry without changing defaults.
165
+ height?: number;
166
+ // OneKey patch: retain the source Android list's measured fractional-DP rounding.
167
+ heightRounding?: 'floor' | 'nearest';
168
+ testID?: string;
169
+ opacity?: number;
170
+ backgroundColor?: string;
171
+ backgroundFullWidth?: boolean;
172
+ pressDisabled?: boolean;
123
173
  key: string;
124
174
  revision?: number;
125
175
  sectionKey?: string;
@@ -136,6 +186,11 @@ export type RowBase = Readonly<{
136
186
  export type IdentityRow = RowBase &
137
187
  Readonly<{
138
188
  type: 'identity';
189
+ // OneKey patch: UTF-16 ranges match Fuse indices and native attributed strings.
190
+ titleMatch?: readonly Readonly<{ start: number; end: number }>[];
191
+ titleActionKey?: string;
192
+ titleActionOnHover?: boolean;
193
+ subtitleSegments?: readonly SelectorTextSegment[];
139
194
  presentation?: 'walletSidebar' | 'accountSelector' | 'networkSelector';
140
195
  leading: LeadingVisual;
141
196
  leadingAction?: Extract<TrailingAccessory, { kind: 'icon' }>;
@@ -257,6 +312,12 @@ export type MetricCardRow = RowBase &
257
312
  export type SectionHeaderRow = RowBase &
258
313
  Readonly<{
259
314
  type: 'sectionHeader';
315
+ // OneKey patch: title help stays independent of section selection.
316
+ sticky?: boolean;
317
+ valueActionTestID?: string;
318
+ valueSegments?: readonly ValueTextSegment[];
319
+ titleActionKey?: string;
320
+ titleActionOnHover?: boolean;
260
321
  sectionKey: string;
261
322
  presentation?: 'networkSelector';
262
323
  indexTitle?: string;
@@ -291,6 +352,13 @@ export type SystemRow = RowBase &
291
352
  message: string;
292
353
  actionKey: string;
293
354
  }>
355
+ | Readonly<{
356
+ type: 'system';
357
+ variant: 'warning';
358
+ title: string;
359
+ message: string;
360
+ borderColor?: string;
361
+ }>
294
362
  | Readonly<{ type: 'system'; variant: 'noMatch'; message: string }>
295
363
  | Readonly<{ type: 'system'; variant: 'end'; message?: string }>
296
364
  | Readonly<{ type: 'system'; variant: 'spacer'; height: number }>
@@ -310,6 +378,11 @@ export type RowModel =
310
378
  | SystemRow;
311
379
 
312
380
  export type NativeListTheme = Readonly<{
381
+ // OneKey patch: explicit selector controls use the original semantic theme tokens.
382
+ checkboxBackground?: string;
383
+ checkboxBorder?: string;
384
+ checkboxIcon?: string;
385
+ cautionBackground?: string;
313
386
  background: string;
314
387
  rowBackground: string;
315
388
  rowSelectedBackground: string;
@@ -329,6 +402,8 @@ export type NativeListTheme = Readonly<{
329
402
  inverseBackground?: string;
330
403
  inverseText?: string;
331
404
  info?: string;
405
+ // OneKey patch: match the existing account warning address tone.
406
+ caution?: string;
332
407
  }>;
333
408
 
334
409
  export type SectionIndexConfig = Readonly<{
@@ -369,7 +444,20 @@ export type NativeListSnapshot = Readonly<{
369
444
  theme?: NativeListTheme;
370
445
  }>;
371
446
 
372
- type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator';
447
+ // OneKey patch: include selector-only mutable presentation fields.
448
+ // type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator';
449
+ // OneKey patch: balance patches also refresh the existing row's spoken content.
450
+ // type CommonPatchFields = 'revision' | 'disabled' | 'selected' | 'separator' | 'height' | 'heightRounding' | 'opacity' | 'pressDisabled';
451
+ type CommonPatchFields =
452
+ | 'revision'
453
+ | 'disabled'
454
+ | 'selected'
455
+ | 'separator'
456
+ | 'height'
457
+ | 'heightRounding'
458
+ | 'opacity'
459
+ | 'pressDisabled'
460
+ | 'accessibilityLabel';
373
461
 
374
462
  export type RowPatch =
375
463
  | Readonly<{
@@ -387,6 +475,10 @@ export type RowPatch =
387
475
  | 'trailing'
388
476
  | 'leading'
389
477
  | 'leadingAction'
478
+ | 'titleMatch'
479
+ | 'titleActionKey'
480
+ | 'titleActionOnHover'
481
+ | 'subtitleSegments'
390
482
  >
391
483
  >;
392
484
  }>
@@ -501,6 +593,8 @@ export type RowPatch =
501
593
  | 'subtitle'
502
594
  | 'value'
503
595
  | 'valueActionKey'
596
+ | 'titleActionKey'
597
+ | 'titleActionOnHover'
504
598
  | 'titleIcon'
505
599
  | 'valueIcon'
506
600
  | 'checkbox'
@@ -565,11 +659,8 @@ export type RowActionEvent = Readonly<{
565
659
  }>;
566
660
 
567
661
  export type ActionAnchorInvalidationReason =
568
- | 'scroll'
569
- | 'rebind'
570
- | 'snapshot'
571
- | 'layout'
572
- | 'destroy';
662
+ // OneKey patch: close web title tooltips when their native list target is left.
663
+ 'pointerLeave' | 'scroll' | 'rebind' | 'snapshot' | 'layout' | 'destroy';
573
664
 
574
665
  export type ActionAnchorInvalidatedEvent = Readonly<{
575
666
  token: string;
package/src/validation.ts CHANGED
@@ -214,6 +214,69 @@ function assertLeadingVisual(
214
214
  assertImage(visual.image, `${path}.image`);
215
215
  assertVisualShape(visual.shape, `${path}.shape`);
216
216
  assertText(visual.cornerIcon?.name, `${path}.cornerIcon.name`);
217
+ // OneKey patch: validate optional selector overlays at the JSON boundary.
218
+ assertText(visual.fallbackIcon?.name, `${path}.fallbackIcon.name`);
219
+ if ((visual.overlays?.length ?? 0) > 2)
220
+ fail(`${path}.overlays`, 'supports at most two overlays');
221
+ visual.overlays?.forEach((overlay, index) => {
222
+ if (!['topLeft', 'bottomRight'].includes(overlay.position))
223
+ fail(
224
+ `${path}.overlays[${index}].position`,
225
+ 'must be topLeft or bottomRight'
226
+ );
227
+ if (
228
+ overlay.size !== undefined &&
229
+ (overlay.size <= 0 || overlay.size > 40)
230
+ )
231
+ fail(`${path}.overlays[${index}].size`, 'must be within 1...40');
232
+ if (
233
+ overlay.padding !== undefined &&
234
+ (!Number.isFinite(overlay.padding) ||
235
+ overlay.padding < 0 ||
236
+ overlay.padding * 2 >= (overlay.size ?? 20))
237
+ )
238
+ fail(
239
+ `${path}.overlays[${index}].padding`,
240
+ 'must fit inside the overlay'
241
+ );
242
+ if (
243
+ overlay.offset !== undefined &&
244
+ (!Number.isFinite(overlay.offset) ||
245
+ overlay.offset < 0 ||
246
+ overlay.offset > 20)
247
+ )
248
+ fail(`${path}.overlays[${index}].offset`, 'must be within 0...20');
249
+ for (const field of ['width', 'height'] as const) {
250
+ const value = overlay[field];
251
+ if (
252
+ value !== undefined &&
253
+ (!Number.isFinite(value) || value <= 0 || value > 40)
254
+ )
255
+ fail(`${path}.overlays[${index}].${field}`, 'must be within 1...40');
256
+ }
257
+ for (const field of ['offsetX', 'offsetY'] as const) {
258
+ const value = overlay[field];
259
+ if (
260
+ value !== undefined &&
261
+ (!Number.isFinite(value) || value < 0 || value > 20)
262
+ )
263
+ fail(`${path}.overlays[${index}].${field}`, 'must be within 0...20');
264
+ }
265
+ if (
266
+ overlay.padding !== undefined &&
267
+ overlay.padding * 2 >=
268
+ Math.min(
269
+ overlay.width ?? overlay.size ?? 20,
270
+ overlay.height ?? overlay.size ?? 20
271
+ )
272
+ )
273
+ fail(
274
+ `${path}.overlays[${index}].padding`,
275
+ 'must fit inside the overlay'
276
+ );
277
+ assertImage(overlay.image, `${path}.overlays[${index}].image`);
278
+ assertText(overlay.text, `${path}.overlays[${index}].text`);
279
+ });
217
280
  if (visual.kind === 'token') {
218
281
  assertImage(visual.networkImage, `${path}.networkImage`);
219
282
  }
@@ -288,6 +351,20 @@ function assertRow(
288
351
  path = `rows[${index}]`
289
352
  ): void {
290
353
  assertKey(row.key, `${path}.key`);
354
+ // OneKey patch: explicit dimensions and opacity cannot corrupt list layout.
355
+ if (row.height !== undefined && (row.height < 0 || row.height > 4096))
356
+ fail(`${path}.height`, 'must be within 0...4096');
357
+ if (
358
+ row.heightRounding !== undefined &&
359
+ (row.height === undefined ||
360
+ !['floor', 'nearest'].includes(row.heightRounding))
361
+ )
362
+ fail(
363
+ `${path}.heightRounding`,
364
+ 'requires an explicit height and must be floor or nearest'
365
+ );
366
+ if (row.opacity !== undefined && (row.opacity < 0 || row.opacity > 1))
367
+ fail(`${path}.opacity`, 'must be within 0...1');
291
368
  if (row.groupId && !row.groupPosition) {
292
369
  fail(`${path}.groupPosition`, 'is required when groupId is present');
293
370
  }
@@ -314,6 +391,43 @@ function assertRow(
314
391
  }
315
392
  assertText(row.title, `${path}.title`);
316
393
  assertText(row.subtitle, `${path}.subtitle`);
394
+ // OneKey patch: selector text segments truncate independently.
395
+ row.subtitleSegments?.forEach((segment, segmentIndex) => {
396
+ assertText(
397
+ segment.text,
398
+ `${path}.subtitleSegments[${segmentIndex}].text`
399
+ );
400
+ if (
401
+ segment.tone !== undefined &&
402
+ ![
403
+ 'primary',
404
+ 'secondary',
405
+ 'disabled',
406
+ 'caution',
407
+ 'positive',
408
+ 'negative',
409
+ ].includes(segment.tone)
410
+ )
411
+ fail(
412
+ `${path}.subtitleSegments[${segmentIndex}].tone`,
413
+ 'invalid selector text tone'
414
+ );
415
+ });
416
+ let previousMatchEnd = 0;
417
+ row.titleMatch?.forEach((match) => {
418
+ if (
419
+ !Number.isInteger(match.start) ||
420
+ !Number.isInteger(match.end) ||
421
+ match.start < previousMatchEnd ||
422
+ match.end <= match.start ||
423
+ match.end > row.title.length
424
+ )
425
+ fail(
426
+ `${path}.titleMatch`,
427
+ 'must contain ordered, non-overlapping UTF-16 ranges inside title'
428
+ );
429
+ previousMatchEnd = match.end;
430
+ });
317
431
  assertText(row.tertiary, `${path}.tertiary`);
318
432
  if (
319
433
  row.tertiaryTone !== undefined &&
@@ -470,13 +584,17 @@ function assertRow(
470
584
  break;
471
585
  case 'system':
472
586
  if (
473
- !['loading', 'retry', 'noMatch', 'end', 'spacer'].includes(row.variant)
587
+ // OneKey patch: deprecated-wallet warnings retain the original scrolling semantics.
588
+ !['loading', 'retry', 'noMatch', 'end', 'spacer', 'warning'].includes(
589
+ row.variant
590
+ )
474
591
  ) {
475
592
  fail(
476
593
  `${path}.variant`,
477
- 'must be loading, retry, noMatch, end, or spacer'
594
+ 'must be loading, retry, noMatch, end, spacer, or warning'
478
595
  );
479
596
  }
597
+ if (row.variant === 'warning') assertText(row.title, `${path}.title`);
480
598
  if (row.variant !== 'spacer') {
481
599
  assertText(row.message, `${path}.message`);
482
600
  }
@@ -649,6 +767,17 @@ export function validateSnapshot(
649
767
 
650
768
  function assertPatchChanges(patch: RowPatch, index: number): void {
651
769
  const path = `patches[${index}].changes`;
770
+ // OneKey patch: partial balance updates retain a valid, current accessibility label.
771
+ if ('accessibilityLabel' in patch.changes) {
772
+ assertText(patch.changes.accessibilityLabel, `${path}.accessibilityLabel`);
773
+ }
774
+ // OneKey patch: partial updates may refer to an existing height but still require a valid policy.
775
+ if (
776
+ 'heightRounding' in patch.changes &&
777
+ patch.changes.heightRounding !== undefined &&
778
+ !['floor', 'nearest'].includes(patch.changes.heightRounding)
779
+ )
780
+ fail(`${path}.heightRounding`, 'must be floor or nearest');
652
781
  if (
653
782
  patch.changes.revision !== undefined &&
654
783
  (!Number.isSafeInteger(patch.changes.revision) ||