@momo-kits/foundation 0.167.1-beta.12 → 0.167.1-beta.14

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 (54) hide show
  1. package/Application/BottomSheet.tsx +246 -42
  2. package/Application/BottomTab/BottomTabBar.tsx +6 -3
  3. package/Application/BottomTab/CustomBottomTabItem.tsx +3 -5
  4. package/Application/BottomTab/DuoBarCompression.tsx +135 -0
  5. package/Application/BottomTab/DuoTabBar.tsx +537 -0
  6. package/Application/BottomTab/index.tsx +145 -134
  7. package/Application/Components/DuoGlassBackground.tsx +158 -0
  8. package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
  9. package/Application/Components/HeaderAnimated.tsx +9 -3
  10. package/Application/Components/HeaderExtendHeader.tsx +70 -28
  11. package/Application/Components/HeaderLeft.tsx +38 -0
  12. package/Application/Components/HeaderRight.tsx +144 -9
  13. package/Application/Components/HeaderTitle.tsx +43 -7
  14. package/Application/Components/SearchHeader.tsx +20 -15
  15. package/Application/Components/SideBarButton.tsx +134 -0
  16. package/Application/ModalScreen.tsx +20 -2
  17. package/Application/NavigationContainer.tsx +22 -38
  18. package/Application/Navigator.ts +1 -3
  19. package/Application/StackScreen.tsx +2 -5
  20. package/Application/device.ts +52 -0
  21. package/Application/types.ts +10 -1
  22. package/Application/utils.tsx +10 -7
  23. package/Badge/Badge.tsx +2 -3
  24. package/Badge/BadgeDotAnimation.tsx +3 -3
  25. package/Button/index.tsx +11 -11
  26. package/Consts/theme.ts +3 -4
  27. package/Input/Input.tsx +3 -4
  28. package/Input/InputMoney.tsx +123 -194
  29. package/Input/InputOTP.tsx +9 -12
  30. package/Input/InputPhoneNumber.tsx +3 -4
  31. package/Input/InputSearch.tsx +17 -14
  32. package/Input/InputTextArea.tsx +3 -4
  33. package/Input/SystemTextInput.tsx +2 -3
  34. package/Input/TextTyping.tsx +11 -6
  35. package/Input/styles.ts +9 -0
  36. package/Input/utils.ts +2 -3
  37. package/Layout/Card.tsx +8 -26
  38. package/Layout/FloatingButton.tsx +3 -3
  39. package/Layout/Item.tsx +1 -10
  40. package/Layout/ReservedRegions.tsx +78 -0
  41. package/Layout/Screen.tsx +242 -48
  42. package/Layout/Section.tsx +5 -18
  43. package/Layout/index.ts +4 -4
  44. package/Layout/styles.ts +5 -0
  45. package/Layout/utils.ts +478 -5
  46. package/Pagination/Dot.tsx +3 -6
  47. package/Popup/PopupNotify.tsx +49 -28
  48. package/Popup/PopupPromotion.tsx +21 -8
  49. package/Popup/utils.ts +26 -0
  50. package/Radio/index.tsx +2 -3
  51. package/Text/utils.ts +2 -3
  52. package/Title/index.tsx +8 -11
  53. package/package.json +5 -2
  54. package/Layout/GridSystem.tsx +0 -137
package/Layout/utils.ts CHANGED
@@ -1,5 +1,11 @@
1
- import React, { Fragment, ReactElement } from 'react';
2
- import { useWindowDimensions } from 'react-native';
1
+ import React, { Fragment, ReactElement, useContext } from 'react';
2
+ import { Platform, useWindowDimensions, ViewStyle } from 'react-native';
3
+ import {
4
+ EdgeInsets,
5
+ SafeAreaInsetsContext,
6
+ } from 'react-native-safe-area-context';
7
+ import { Spacing } from '../Consts';
8
+ import { ReservedRegion, ReservedRegionsContext } from './ReservedRegions';
3
9
 
4
10
  /**
5
11
  * validate children type
@@ -38,10 +44,477 @@ const validateChildren = (
38
44
  }
39
45
  };
40
46
 
47
+ /**
48
+ * Header geometry
49
+ * - barHeight: navigation bar row below the status bar
50
+ * - iPhoneMinHeight: header never shorter than this on iPhone (landscape, iPhone Duo)
51
+ * - statusBarMinInset: a smaller top inset means the status bar is not on the top edge
52
+ * - largeWidth / largeMargin: @react-navigation/elements Header gives its content row
53
+ * 5pt side margins on iOS from 414pt wide
54
+ */
55
+ const HeaderMetrics = {
56
+ barHeight: 52,
57
+ iPhoneMinHeight: 72,
58
+ statusBarMinInset: 20,
59
+ largeWidth: 414,
60
+ largeMargin: 5,
61
+ };
62
+
63
+ /**
64
+ * iPhone Duo geometry, iOS 27.1 (Xcode 27.1 simulator)
65
+ * - maxAspectRatio: both displays are near 1.42 (inner) and 1.45 (outer), every slab
66
+ * iPhone is 1.77 or taller
67
+ * - innerMinHeight: window height from which the inner display is in use (outer display
68
+ * is 466pt tall in landscape, inner display 669pt)
69
+ * - controlSize / itemPitch: vertical bar of 48pt circular controls, item groups as 48pt
70
+ * wide capsules at a 50pt pitch, 12pt from the strip's inner edge and 12pt apart
71
+ * - firstItemTop: with the status bar in the strip, the first control sits under the
72
+ * camera, clock and connectivity cluster (cluster ends near y 151 on the outer display,
73
+ * y 98 on the inner display in landscape)
74
+ * - outerHeaderHeight / outerCameraCenterY: on the outer display in portrait the header
75
+ * is 4pt taller than iPhoneMinHeight and its title lines up with the camera punchout
76
+ * (y 24–66 at the top of the strip)
77
+ * - edgeInset: on the outer display in landscape the strip holds no status bar, so its
78
+ * controls start this far from the top and stay this far above the bottom, unless a
79
+ * reserved region (the camera at either end) asks for more
80
+ * - innerStatusBarCenterY / innerStatusBarWidth: on the inner display in portrait the
81
+ * clock and connectivity cluster sits at the top right corner (x 550–643 of 669, clock
82
+ * digits centered on y 47); its reserved region, when reported, is taller than the glyphs
83
+ * - floatingTabBarHeight / floatingTabBarMargin / floatingTabBarInsetOverlap: the host
84
+ * app's iOS 26 system tab bar on the inner display in portrait, a 62pt Liquid Glass pill
85
+ * sized to its tabs with the QR as a pinned 62pt platter beside it, centered and at
86
+ * least 20pt from the side edges; it overlaps the home indicator inset by 13pt (21pt
87
+ * above the screen bottom with a 34pt inset)
88
+ */
89
+ const DuoMetrics = {
90
+ maxAspectRatio: 1.6,
91
+ innerMinHeight: 600,
92
+ controlSize: 48,
93
+ itemPitch: 50,
94
+ firstItemTop: 170,
95
+ firstItemTopLandscape: 120,
96
+ outerHeaderHeight: 76,
97
+ outerCameraCenterY: 45,
98
+ edgeInset: 24,
99
+ innerStatusBarCenterY: 47,
100
+ innerStatusBarWidth: 120,
101
+ floatingTabBarHeight: 62,
102
+ floatingTabBarMargin: 20,
103
+ floatingTabBarInsetOverlap: 13,
104
+ };
105
+
106
+ const NO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
107
+
108
+ /**
109
+ * how far system regions reach into a side strip from its top and from its bottom end
110
+ * (e.g. the camera, which sits at either end depending on the rotation), and the center
111
+ * y of the region reaching furthest from the top
112
+ */
113
+ const getStripOcclusion = (
114
+ regions: ReservedRegion[],
115
+ stripLeft: number,
116
+ stripRight: number,
117
+ windowHeight: number,
118
+ ) =>
119
+ regions.reduce(
120
+ (occlusion, region) => {
121
+ const overlapsStrip =
122
+ region.x < stripRight && region.x + region.width > stripLeft;
123
+ if (!overlapsStrip) {
124
+ return occlusion;
125
+ }
126
+ const centerY = region.y + region.height / 2;
127
+ if (centerY < windowHeight / 2) {
128
+ if (region.y + region.height > occlusion.top) {
129
+ occlusion.top = region.y + region.height;
130
+ occlusion.topCenter = centerY;
131
+ }
132
+ } else {
133
+ occlusion.bottom = Math.max(occlusion.bottom, windowHeight - region.y);
134
+ }
135
+ return occlusion;
136
+ },
137
+ { top: 0, topCenter: 0, bottom: 0 },
138
+ );
139
+
140
+ /**
141
+ * how far the clock and battery clusters of a status bar on the top edge reach in from
142
+ * each side. `undefined` without reserved regions there
143
+ */
144
+ const getStatusBarClusters = (
145
+ regions: ReservedRegion[],
146
+ windowWidth: number,
147
+ topInset: number,
148
+ ) => {
149
+ const midX = windowWidth / 2;
150
+ const clusters = regions.filter(
151
+ region =>
152
+ region.y < topInset &&
153
+ region.y + region.height <= topInset &&
154
+ (region.x + region.width <= midX || region.x >= midX),
155
+ );
156
+ if (!clusters.length) {
157
+ return undefined;
158
+ }
159
+
160
+ return clusters.reduce(
161
+ (result, region) => {
162
+ if (region.x < midX) {
163
+ result.left = Math.max(result.left, region.x + region.width);
164
+ } else {
165
+ result.right = Math.max(result.right, windowWidth - region.x);
166
+ }
167
+ return result;
168
+ },
169
+ { left: 0, right: 0 },
170
+ );
171
+ };
172
+
173
+ const isIPhone = () =>
174
+ Platform.OS === 'ios' && Platform.constants.interfaceIdiom === 'phone';
175
+
176
+ /**
177
+ * Header height and the room it keeps above the bar row for the status bar. On iPhone the
178
+ * header is at least iPhoneMinHeight, and with the status bar off the top edge the row
179
+ * spans the whole header, so the title stays vertically centered.
180
+ */
181
+ const getHeaderLayout = (topInset: number) => {
182
+ const height = HeaderMetrics.barHeight + topInset;
183
+ if (!isIPhone()) {
184
+ return { headerHeight: height, headerStatusBarHeight: topInset };
185
+ }
186
+
187
+ const statusBarOffTop = topInset < HeaderMetrics.statusBarMinInset;
188
+ return {
189
+ headerHeight: Math.max(height, HeaderMetrics.iPhoneMinHeight),
190
+ headerStatusBarHeight: statusBarOffTop ? 0 : topInset,
191
+ };
192
+ };
193
+
194
+ /**
195
+ * Header whose bar row is centered on a window y, at least iPhoneMinHeight tall and
196
+ * reaching 8pt below a side bar control centered there
197
+ */
198
+ const getCenteredHeaderLayout = (centerY: number) => {
199
+ const headerHeight = Math.max(
200
+ HeaderMetrics.iPhoneMinHeight,
201
+ centerY + DuoMetrics.controlSize / 2 + Spacing.S,
202
+ );
203
+ return {
204
+ headerHeight,
205
+ headerStatusBarHeight: Math.max(0, centerY * 2 - headerHeight),
206
+ };
207
+ };
208
+
209
+ export type SideBarEdge = 'left' | 'right';
210
+
211
+ export type SideBarLayout = {
212
+ /**
213
+ * Edge holding the side bar, the one with the larger safe-area inset.
214
+ */
215
+ edge: SideBarEdge;
216
+
217
+ /**
218
+ * Width of the side bar column, the safe-area inset on that edge.
219
+ */
220
+ width: number;
221
+
222
+ /**
223
+ * Window y of the first side bar item, below the navigation bar and the side status bar.
224
+ */
225
+ top: number;
226
+
227
+ /**
228
+ * Room to keep free above the window bottom, clear of a camera at the bottom end.
229
+ */
230
+ bottom: number;
231
+ };
232
+
233
+ export type ScreenLayout = {
234
+ /**
235
+ * Window width, from useWindowDimensions.
236
+ */
237
+ width: number;
238
+
239
+ /**
240
+ * Window height, from useWindowDimensions.
241
+ */
242
+ height: number;
243
+
244
+ /**
245
+ * `true` when running on iPhone Duo, outer or inner display.
246
+ */
247
+ isIPhoneDuo: boolean;
248
+
249
+ /**
250
+ * Raw safe-area insets.
251
+ */
252
+ insets: EdgeInsets;
253
+
254
+ /**
255
+ * Horizontal insets the screen content keeps clear of. 0 outside iPhone Duo.
256
+ */
257
+ insetLeft: number;
258
+ insetRight: number;
259
+
260
+ /**
261
+ * Width available to the screen content: window width minus the horizontal insets.
262
+ */
263
+ contentWidth: number;
264
+
265
+ /**
266
+ * Header height: the top safe-area inset plus the navigation bar row, at least 72pt on iPhone.
267
+ */
268
+ headerHeight: number;
269
+
270
+ /**
271
+ * Room above the navigation bar row for the status bar, the rest of headerHeight is
272
+ * the row, which centers the title vertically. 0 when the status bar is not on top.
273
+ */
274
+ headerStatusBarHeight: number;
275
+
276
+ /**
277
+ * Side column holding the back button and the toolkit, when iPhone Duo moves the
278
+ * bars to the side. `undefined` while the bars stay horizontal.
279
+ */
280
+ sideBar?: SideBarLayout;
281
+
282
+ /**
283
+ * Room the header row keeps from each side for the clock and battery clusters, when
284
+ * iPhone Duo puts the row on the status bar (inner display in portrait).
285
+ */
286
+ statusBarClearance?: StatusBarClearance;
287
+ };
288
+
289
+ export type StatusBarClearance = {
290
+ left: number;
291
+ right: number;
292
+ };
293
+
294
+ /**
295
+ * Detect iPhone Duo from the window shape and the safe area: a phone idiom window
296
+ * near square, or a phone with asymmetric side insets (slab iPhones report equal
297
+ * left / right insets in landscape, iPhone Duo moves the status bar to one side).
298
+ */
299
+ const isIPhoneDuoLayout = (
300
+ width: number,
301
+ height: number,
302
+ insets: EdgeInsets,
303
+ ) => {
304
+ if (!isIPhone()) {
305
+ return false;
306
+ }
307
+
308
+ const shortSide = Math.min(width, height);
309
+ const aspectRatio = shortSide > 0 ? Math.max(width, height) / shortSide : 0;
310
+ const isSquare = aspectRatio > 0 && aspectRatio < DuoMetrics.maxAspectRatio;
311
+
312
+ return isSquare || insets.left !== insets.right;
313
+ };
314
+
315
+ /**
316
+ * Screen layout for the current window: iPhone Duo detection, horizontal content insets,
317
+ * content width and the side bar. Follows the window through fold, unfold, rotation and
318
+ * Split View since it reads useWindowDimensions and the safe-area context.
319
+ */
320
+ const useScreenLayout = (): ScreenLayout => {
321
+ const { width, height } = useWindowDimensions();
322
+ const insets = useContext(SafeAreaInsetsContext) ?? NO_INSETS;
323
+ const reservedRegions = useContext(ReservedRegionsContext);
324
+
325
+ let headerLayout = getHeaderLayout(insets.top);
326
+ const isIPhoneDuo = isIPhoneDuoLayout(width, height, insets);
327
+ if (!isIPhoneDuo) {
328
+ return {
329
+ width,
330
+ height,
331
+ isIPhoneDuo,
332
+ insets,
333
+ insetLeft: 0,
334
+ insetRight: 0,
335
+ contentWidth: width,
336
+ ...headerLayout,
337
+ };
338
+ }
339
+
340
+ let statusBarClearance: StatusBarClearance | undefined;
341
+ const isInnerPortrait = height > width && width >= DuoMetrics.innerMinHeight;
342
+ if (isInnerPortrait && insets.top >= HeaderMetrics.statusBarMinInset) {
343
+ // the row shares the status bar band, between its clusters
344
+ const measured = getStatusBarClusters(reservedRegions, width, insets.top);
345
+ const fallbackRight = DuoMetrics.innerStatusBarWidth + Spacing.S;
346
+ const clusters = measured ?? { left: 0, right: fallbackRight };
347
+ // center on the clock digits, the region center sits above them
348
+ headerLayout = {
349
+ headerHeight: DuoMetrics.innerStatusBarCenterY * 2,
350
+ headerStatusBarHeight: 0,
351
+ };
352
+ statusBarClearance = clusters;
353
+ }
354
+
355
+ const sideInset = Math.max(insets.left, insets.right);
356
+ let sideBar: SideBarLayout | undefined;
357
+ // narrower strips cannot host a control 12pt from the inner edge
358
+ if (sideInset >= DuoMetrics.controlSize + Spacing.M) {
359
+ const edge = insets.right >= insets.left ? 'right' : 'left';
360
+ const stripLeft = edge === 'right' ? width - sideInset : 0;
361
+ // keep controls clear of the camera and of the status bar cluster, which grows for
362
+ // calls and Live Activities. Region frames already include the system's margin
363
+ const occlusion = getStripOcclusion(
364
+ reservedRegions,
365
+ stripLeft,
366
+ stripLeft + sideInset,
367
+ height,
368
+ );
369
+
370
+ let top = insets.top + HeaderMetrics.barHeight + Spacing.M;
371
+ if (statusBarClearance) {
372
+ top = headerLayout.headerHeight + Spacing.M;
373
+ }
374
+ if (insets.top < HeaderMetrics.statusBarMinInset) {
375
+ // status bar lives in the strip: start under its cluster, as the native bar does
376
+ const isLandscape = width > height;
377
+ if (!isLandscape) {
378
+ top = DuoMetrics.firstItemTop;
379
+ } else if (height >= DuoMetrics.innerMinHeight) {
380
+ top = DuoMetrics.firstItemTopLandscape;
381
+ } else {
382
+ // outer display in landscape: no status bar in the strip, the title row centers
383
+ // on the camera when it sits at the top end, otherwise on the back button
384
+ top = DuoMetrics.edgeInset;
385
+ const cameraAtTop = occlusion.top > top;
386
+ const backCenterY = top + DuoMetrics.controlSize / 2;
387
+ const titleCenterY = cameraAtTop ? occlusion.topCenter : backCenterY;
388
+ headerLayout = getCenteredHeaderLayout(titleCenterY);
389
+ }
390
+
391
+ if (height > width && width < DuoMetrics.innerMinHeight) {
392
+ // outer display: the row starts below the top so the title centers on the camera
393
+ headerLayout = {
394
+ headerHeight: DuoMetrics.outerHeaderHeight,
395
+ headerStatusBarHeight:
396
+ DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight,
397
+ };
398
+ }
399
+ }
400
+
401
+ sideBar = {
402
+ edge,
403
+ width: sideInset,
404
+ top: Math.max(top, occlusion.top),
405
+ bottom: Math.max(DuoMetrics.edgeInset, occlusion.bottom),
406
+ };
407
+ }
408
+
409
+ return {
410
+ width,
411
+ height,
412
+ isIPhoneDuo,
413
+ insets,
414
+ insetLeft: insets.left,
415
+ insetRight: insets.right,
416
+ contentWidth: width - insets.left - insets.right,
417
+ ...headerLayout,
418
+ sideBar,
419
+ statusBarClearance,
420
+ };
421
+ };
422
+
423
+ /**
424
+ * Header row options for react-navigation: its height, the room for the status bar, and
425
+ * on iPhone Duo inner portrait the buttons kept clear of the clock and battery clusters
426
+ */
427
+ const getHeaderRowOptions = ({
428
+ headerHeight,
429
+ headerStatusBarHeight,
430
+ statusBarClearance,
431
+ sideBar,
432
+ }: ScreenLayout) => {
433
+ const clearsStatusBar = statusBarClearance && !sideBar;
434
+ return {
435
+ headerStyle: { height: headerHeight },
436
+ headerStatusBarHeight,
437
+ // side bar controls are absolute within the containers, padding would not move them
438
+ ...(clearsStatusBar && {
439
+ headerLeftContainerStyle: { paddingLeft: statusBarClearance.left },
440
+ headerRightContainerStyle: { paddingRight: statusBarClearance.right },
441
+ }),
442
+ };
443
+ };
444
+
445
+ /**
446
+ * Side margin @react-navigation/elements Header adds to its content row
447
+ */
448
+ const getHeaderMargin = (width: number) => {
449
+ if (Platform.OS === 'ios' && width >= HeaderMetrics.largeWidth) {
450
+ return HeaderMetrics.largeMargin;
451
+ }
452
+ return 0;
453
+ };
454
+
455
+ const InnerEdgeStyles: Record<SideBarEdge, ViewStyle> = {
456
+ right: { alignItems: 'flex-start', paddingLeft: Spacing.M },
457
+ left: { alignItems: 'flex-end', paddingRight: Spacing.M },
458
+ };
459
+
460
+ /**
461
+ * Absolute style that floats a header item into the side bar, relative to the header
462
+ * container (left or right) rendering it, 12pt from the strip's inner edge.
463
+ * `undefined` while there is no side bar.
464
+ * @param container header container rendering the item
465
+ * @param slot position in the side bar, 0 for back, 1 for the toolkit below it
466
+ */
467
+ const useSideBarItemStyle = (
468
+ container: 'left' | 'right',
469
+ slot: number,
470
+ ): ViewStyle | undefined => {
471
+ const { width, insets, headerStatusBarHeight, sideBar } = useScreenLayout();
472
+ if (!sideBar) {
473
+ return undefined;
474
+ }
475
+
476
+ const margin = getHeaderMargin(width);
477
+ const columnLeft = sideBar.edge === 'left' ? 0 : width - sideBar.width;
478
+ const top =
479
+ sideBar.top -
480
+ headerStatusBarHeight +
481
+ slot * (DuoMetrics.controlSize + Spacing.M);
482
+ const innerEdgeStyle = InnerEdgeStyles[sideBar.edge];
483
+
484
+ if (container === 'left') {
485
+ return {
486
+ position: 'absolute',
487
+ top,
488
+ left: columnLeft - margin - insets.left,
489
+ width: sideBar.width,
490
+ ...innerEdgeStyle,
491
+ };
492
+ }
493
+
494
+ const containerRight = width - margin - insets.right;
495
+ return {
496
+ position: 'absolute',
497
+ top,
498
+ right: containerRight - (columnLeft + sideBar.width),
499
+ width: sideBar.width,
500
+ ...innerEdgeStyle,
501
+ };
502
+ };
503
+
41
504
  const useGridSystem = () => {
42
- const windowSize = useWindowDimensions();
43
- const numberOfColumns = windowSize.width > 600 ? 24 : 12;
505
+ const { contentWidth } = useScreenLayout();
506
+ const numberOfColumns = contentWidth > 600 ? 24 : 12;
44
507
  return { numberOfColumns };
45
508
  };
46
509
 
47
- export { useGridSystem, validateChildren };
510
+ export {
511
+ DuoMetrics,
512
+ HeaderMetrics,
513
+ InnerEdgeStyles,
514
+ getHeaderMargin,
515
+ getHeaderRowOptions,
516
+ useGridSystem,
517
+ useScreenLayout,
518
+ useSideBarItemStyle,
519
+ validateChildren,
520
+ };
@@ -6,12 +6,9 @@ import { ApplicationContext } from '../Context';
6
6
 
7
7
  const Dot: FC<DotProps> = ({ active, style }) => {
8
8
  const { theme } = useContext(ApplicationContext);
9
- const dotStyle = active
10
- ? [styles.activeDot]
11
- : [
12
- styles.inactiveDot,
13
- { backgroundColor: theme.colors.background.pressed },
14
- ];
9
+ const pressedColor = theme.colors.background.pressed;
10
+ const inactiveStyle = [styles.inactiveDot, { backgroundColor: pressedColor }];
11
+ const dotStyle = active ? [styles.activeDot] : inactiveStyle;
15
12
 
16
13
  return <View style={[style, dotStyle]} />;
17
14
  };
@@ -1,11 +1,5 @@
1
1
  import React, { useContext, useRef, useState } from 'react';
2
- import {
3
- Dimensions,
4
- ScrollView,
5
- StyleSheet,
6
- TouchableOpacity,
7
- View,
8
- } from 'react-native';
2
+ import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
9
3
  import { ApplicationContext, MiniAppContext } from '../Context';
10
4
  import { Button } from '../Button';
11
5
  import { Colors, Radius, Spacing, Styles } from '../Consts';
@@ -13,7 +7,8 @@ import { Icon } from '../Icon';
13
7
  import { Image } from '../Image';
14
8
  import { Text, useScaleSize } from '../Text';
15
9
  import { PopupNotifyProps } from './types';
16
- import { runOnJS } from 'react-native-reanimated';
10
+ import { scheduleOnRN } from 'react-native-worklets';
11
+ import { getPopupRadius, usePopupMaxHeight, usePopupWidth } from './utils';
17
12
 
18
13
  const PopupNotify: React.FC<PopupNotifyProps> = ({
19
14
  image,
@@ -29,7 +24,9 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
29
24
  const context = useContext<any>(MiniAppContext);
30
25
  const { theme, navigator, translate } = useContext(ApplicationContext);
31
26
  const [scrollContent, setScrollContent] = useState(false);
32
- const { width: widthDevice } = Dimensions.get('window');
27
+ const popupWidth = usePopupWidth();
28
+ const maxHeight = usePopupMaxHeight();
29
+ const radius = getPopupRadius();
33
30
  const scaledMaxHeight = useScaleSize(172);
34
31
 
35
32
  const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false;
@@ -50,7 +47,7 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
50
47
  onRequestClose?.(callback);
51
48
  } else {
52
49
  navigator?.pop();
53
- runOnJS(() => callback?.())();
50
+ scheduleOnRN(() => callback?.());
54
51
  }
55
52
  };
56
53
 
@@ -172,19 +169,17 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
172
169
  }
173
170
  };
174
171
 
175
- return (
176
- <View
177
- accessibilityLabel={'popup_notify'}
178
- style={[
179
- styles.container,
180
- {
181
- backgroundColor: theme.colors.background.surface,
182
- width: widthDevice - Spacing.XL * 2,
183
- },
184
- showBaseLineDebug && styles.debugBaseLine,
185
- ]}
186
- >
187
- {!!image && <Image source={{ uri: image }} style={styles.image} />}
172
+ const body = (
173
+ <>
174
+ {!!image && (
175
+ <Image
176
+ source={{ uri: image }}
177
+ style={[
178
+ styles.image,
179
+ { borderTopLeftRadius: radius, borderTopRightRadius: radius },
180
+ ]}
181
+ />
182
+ )}
188
183
  <View style={styles.content}>
189
184
  <Text
190
185
  typography={'header_default_bold'}
@@ -225,6 +220,36 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
225
220
  </View>
226
221
  )}
227
222
  </View>
223
+ </>
224
+ );
225
+
226
+ return (
227
+ <View
228
+ accessibilityLabel={'popup_notify'}
229
+ style={[
230
+ {
231
+ backgroundColor: theme.colors.background.surface,
232
+ borderRadius: radius,
233
+ width: popupWidth,
234
+ maxHeight,
235
+ },
236
+ showBaseLineDebug && styles.debugBaseLine,
237
+ ]}
238
+ >
239
+ {maxHeight === undefined ? (
240
+ body
241
+ ) : (
242
+ // scrolls when iPhone Duo landscape is too short; actions stay visible
243
+ <ScrollView
244
+ style={[
245
+ styles.body,
246
+ { borderTopLeftRadius: radius, borderTopRightRadius: radius },
247
+ ]}
248
+ bounces={false}
249
+ >
250
+ {body}
251
+ </ScrollView>
252
+ )}
228
253
  <View style={styles.contentAction}>{buildAction()}</View>
229
254
  {buildCloseIcon()}
230
255
  </View>
@@ -232,16 +257,12 @@ const PopupNotify: React.FC<PopupNotifyProps> = ({
232
257
  };
233
258
 
234
259
  const styles = StyleSheet.create({
235
- container: {
236
- borderRadius: Radius.L,
237
- },
238
260
  debugBaseLine: { borderWidth: 1, borderColor: Colors.green_06 },
239
261
  image: {
240
- borderTopLeftRadius: Radius.L,
241
- borderTopRightRadius: Radius.L,
242
262
  aspectRatio: 1.7777,
243
263
  overflow: 'hidden',
244
264
  },
265
+ body: { flexShrink: 1, overflow: 'hidden' },
245
266
  content: { padding: Spacing.XL },
246
267
  description: {
247
268
  marginTop: Spacing.S,