@oxyhq/services 22.11.1 → 22.12.1

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 (68) hide show
  1. package/lib/commonjs/ui/hooks/useSurfaceHeader.js +8 -6
  2. package/lib/commonjs/ui/hooks/useSurfaceHeader.js.map +1 -1
  3. package/lib/commonjs/ui/navigation/surfaceRegistry.js +11 -37
  4. package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -1
  5. package/lib/commonjs/ui/screens/AvatarCropScreen.js +9 -13
  6. package/lib/commonjs/ui/screens/AvatarCropScreen.js.map +1 -1
  7. package/lib/commonjs/ui/screens/FileManagementScreen.js +66 -18
  8. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  9. package/lib/commonjs/ui/screens/PaymentGatewayScreen.js +11 -0
  10. package/lib/commonjs/ui/screens/PaymentGatewayScreen.js.map +1 -1
  11. package/lib/commonjs/ui/screens/ProfileScreen.js +91 -117
  12. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  13. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +22 -54
  14. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  15. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +38 -154
  16. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  17. package/lib/module/ui/hooks/useSurfaceHeader.js +8 -6
  18. package/lib/module/ui/hooks/useSurfaceHeader.js.map +1 -1
  19. package/lib/module/ui/navigation/surfaceRegistry.js +11 -37
  20. package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -1
  21. package/lib/module/ui/screens/AvatarCropScreen.js +9 -13
  22. package/lib/module/ui/screens/AvatarCropScreen.js.map +1 -1
  23. package/lib/module/ui/screens/FileManagementScreen.js +66 -18
  24. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  25. package/lib/module/ui/screens/PaymentGatewayScreen.js +11 -0
  26. package/lib/module/ui/screens/PaymentGatewayScreen.js.map +1 -1
  27. package/lib/module/ui/screens/ProfileScreen.js +93 -119
  28. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  29. package/lib/module/ui/screens/WelcomeNewUserScreen.js +23 -54
  30. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  31. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +38 -154
  32. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  33. package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts +7 -5
  34. package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +8 -9
  36. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/ui/screens/AvatarCropScreen.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts +1 -1
  39. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/ui/screens/PaymentGatewayScreen.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/ui/screens/WelcomeNewUserScreen.d.ts +2 -1
  43. package/lib/typescript/commonjs/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +0 -6
  45. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  46. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts +7 -5
  47. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
  48. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +8 -9
  49. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  50. package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts.map +1 -1
  51. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts +1 -1
  52. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  53. package/lib/typescript/module/ui/screens/PaymentGatewayScreen.d.ts.map +1 -1
  54. package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
  55. package/lib/typescript/module/ui/screens/WelcomeNewUserScreen.d.ts +2 -1
  56. package/lib/typescript/module/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  57. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +0 -6
  58. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  59. package/package.json +4 -4
  60. package/src/ui/hooks/useSurfaceHeader.ts +21 -16
  61. package/src/ui/navigation/__tests__/getSurfaceConfig.test.ts +16 -2
  62. package/src/ui/navigation/surfaceRegistry.ts +14 -42
  63. package/src/ui/screens/AvatarCropScreen.tsx +14 -23
  64. package/src/ui/screens/FileManagementScreen.tsx +76 -29
  65. package/src/ui/screens/PaymentGatewayScreen.tsx +11 -0
  66. package/src/ui/screens/ProfileScreen.tsx +96 -111
  67. package/src/ui/screens/WelcomeNewUserScreen.tsx +23 -63
  68. package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +39 -171
@@ -25,13 +25,10 @@ import FileLibraryError from './FileLibraryError';
25
25
  * import. Matches the pattern used by AvatarCropScreen.
26
26
  */
27
27
  type HapticImpact = 'light' | 'medium' | 'heavy';
28
- type HapticNotification = 'success' | 'warning' | 'error';
29
28
  interface HapticsModule {
30
29
  impactAsync: (style: unknown) => Promise<void>;
31
- notificationAsync: (type: unknown) => Promise<void>;
32
30
  selectionAsync: () => Promise<void>;
33
31
  ImpactFeedbackStyle: { Light: unknown; Medium: unknown; Heavy: unknown };
34
- NotificationFeedbackType: { Success: unknown; Warning: unknown; Error: unknown };
35
32
  }
36
33
 
37
34
  let hapticsModulePromise: Promise<HapticsModule | null> | null = null;
@@ -76,35 +73,23 @@ const hapticSelection = async (): Promise<void> => {
76
73
  }
77
74
  };
78
75
 
79
- const hapticNotification = async (type: HapticNotification): Promise<void> => {
80
- const h = await getHaptics();
81
- if (!h) return;
82
- const typeEnum =
83
- type === 'error'
84
- ? h.NotificationFeedbackType.Error
85
- : type === 'warning'
86
- ? h.NotificationFeedbackType.Warning
87
- : h.NotificationFeedbackType.Success;
88
- try {
89
- await h.notificationAsync(typeEnum);
90
- } catch {
91
- // Silent.
92
- }
93
- };
94
-
95
76
  /**
96
- * The picker renders inside a Bloom `<Dialog>` (bottom-sheet on narrow, centered
97
- * card on `md+`). The Dialog owns its own chrome: it draws its OWN drag handle
98
- * in bottom placement (a floating 28dp pill that paints ABOVE this header via a
99
- * higher z-index) and none in center placement — so the picker's translucent
100
- * header no longer reserves the handle strip itself. The header is just the 56dp
101
- * app bar; grid content is pushed down by the app bar + 4dp of breathing room so
102
- * the first row clears it. These are fixed layout constants — the equivalent
103
- * `min-h-[56px]` / `pt-[60px]` utility classes in the JSX MUST stay in sync.
77
+ * The picker renders inside a Bloom `<Dialog>` in NAV-HEADER mode with
78
+ * `tone: 'onImage'` (bottom-sheet on narrow, centered card on `md+`). The Dialog
79
+ * owns the shared gradient nav bar (Cancel / "Choose Photo" / Upload, wired by
80
+ * `FileManagementScreen` through `useSurfaceHeader`). Because the tone is
81
+ * `'onImage'`, the Dialog inserts NO nav-bar spacer above this content
82
+ * (`DialogNavBarSpacer` collapses to nothing), so the black photo grid slides UP
83
+ * under the floating translucent bar and reads EDGE-TO-EDGE — the Apple/Google
84
+ * Photos immersive picker. The grid therefore takes the FULL `frameSize` height
85
+ * and carries no top padding; only the pull-to-refresh spinner is offset so it
86
+ * lands below the floating bar.
87
+ *
88
+ * `FLOATING_NAV_BAR_HEIGHT` mirrors Bloom's `DIALOG_NAV_BAR_HEIGHT` (not exported
89
+ * from `@oxyhq/bloom/dialog`) — the height of the floating bar the refresh spinner
90
+ * must clear.
104
91
  */
105
- const APP_BAR_HEIGHT = 56;
106
- const HEADER_HEIGHT = APP_BAR_HEIGHT; // 56
107
- const GRID_CONTENT_PADDING_TOP = HEADER_HEIGHT + 4; // 60
92
+ const FLOATING_NAV_BAR_HEIGHT = 52;
108
93
 
109
94
  /**
110
95
  * Props for the dedicated photo picker view. Used by FileManagementScreen
@@ -120,7 +105,6 @@ export interface PhotoPickerViewProps {
120
105
  refreshing: boolean;
121
106
  uploading: boolean;
122
107
  isPickingDocument: boolean;
123
- uploadProgress: { current: number; total: number } | null;
124
108
  hasMore: boolean;
125
109
  loadingMore: boolean;
126
110
  /**
@@ -143,8 +127,6 @@ export interface PhotoPickerViewProps {
143
127
  onUpload: () => void;
144
128
  onRefresh: () => void;
145
129
  onLoadMore: () => void;
146
- onCancel: () => void;
147
- onConfirm: () => void;
148
130
  t: (key: string, vars?: Record<string, string | number>) => string;
149
131
  }
150
132
 
@@ -362,7 +344,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
362
344
  refreshing,
363
345
  uploading,
364
346
  isPickingDocument,
365
- uploadProgress,
366
347
  hasMore,
367
348
  loadingMore,
368
349
  loading,
@@ -377,8 +358,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
377
358
  onUpload,
378
359
  onRefresh,
379
360
  onLoadMore,
380
- onCancel,
381
- onConfirm,
382
361
  t,
383
362
  }) => {
384
363
  // Grid sizing width. We PREFER the measured grid-container width (the
@@ -400,11 +379,14 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
400
379
 
401
380
  // The picker FILLS its panel: the surface morphs the container UP to an
402
381
  // explicit large target (`frameSize.heightRatio: 0.9` in `surfaceRegistry`),
403
- // and this root takes that SAME 0.9-of-viewport height so the grid uses the
404
- // whole grown card (a `flex: 1` root can't fill no definite bound propagates
405
- // through the sheet's flex chain, and the panel is pinned, not measured). The
406
- // FlatList then scrolls within this fixed height. Keep the 0.9 ratio in sync
407
- // with the picker's `frameSize` in `surfaceRegistry.ts`.
382
+ // and this root takes that SAME 0.9-of-viewport height. Under `tone:'onImage'`
383
+ // the Dialog inserts NO nav-bar spacer, so the grid uses the FULL morph target
384
+ // (no `-52`) and slides up under the floating bar, edge-to-edge, within the
385
+ // panel's `maxHeightRatio: 0.9` cap. A `flex: 1` root can't fill (no definite
386
+ // bound propagates through the sheet's flex chain at rest, where the panel is
387
+ // height:auto), so the fixed height is required; the FlatList scrolls within
388
+ // it. Keep the 0.9 ratio in sync with the picker's `frameSize` in
389
+ // `surfaceRegistry.ts`.
408
390
  const panelHeight = useMemo(() => Math.round(windowHeight * 0.9), [windowHeight]);
409
391
  const rootStyle = useMemo(() => ({ height: panelHeight }), [panelHeight]);
410
392
 
@@ -421,7 +403,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
421
403
  const skeletonTileCount = useMemo(() => {
422
404
  const rowHeight = cellSize + gutter;
423
405
  if (rowHeight <= 0) return columns * SKELETON_MIN_ROWS;
424
- const available = Math.max(0, panelHeight - GRID_CONTENT_PADDING_TOP);
406
+ const available = Math.max(0, panelHeight - FLOATING_NAV_BAR_HEIGHT);
425
407
  const rows = Math.max(SKELETON_MIN_ROWS, Math.floor(available / rowHeight));
426
408
  return rows * columns;
427
409
  }, [cellSize, gutter, columns, panelHeight]);
@@ -440,9 +422,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
440
422
 
441
423
  const hasAnySelection = selectedIds.size > 0;
442
424
 
443
- // Compact icon-only upload pill on narrow sheets; full pill otherwise.
444
- const showUploadLabel = effectiveWidth >= 360;
445
-
446
425
  const isEmpty = photos.length === 0;
447
426
  const atSelectionLimit =
448
427
  multiSelect && maxSelection != null && selectedIds.size >= maxSelection;
@@ -467,16 +446,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
467
446
  [onPreviewPhoto],
468
447
  );
469
448
 
470
- const handleConfirm = useCallback(() => {
471
- if (multiSelect) {
472
- if (selectedIds.size === 0) return;
473
- if (Platform.OS !== 'web') {
474
- void hapticNotification('success');
475
- }
476
- }
477
- onConfirm();
478
- }, [multiSelect, selectedIds.size, onConfirm]);
479
-
480
449
  // FlatList renderItem: each cell knows its enterIndex (for stagger).
481
450
  const renderItem = useCallback(
482
451
  ({ item, index }: { item: FileMetadata; index: number }) => {
@@ -537,16 +506,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
537
506
  onLoadMore();
538
507
  }, [loadingMore, hasMore, onLoadMore]);
539
508
 
540
- const confirmDisabled = multiSelect && selectedIds.size === 0;
541
- const confirmLabel = multiSelect
542
- ? t('fileManagement.doneWithCount', { count: selectedIds.size })
543
- : t('fileManagement.done');
544
-
545
- // The progress fill width. Guard against zero division.
546
- const progressFraction = uploadProgress && uploadProgress.total > 0
547
- ? Math.min(1, Math.max(0, uploadProgress.current / uploadProgress.total))
548
- : 0;
549
-
550
509
  return (
551
510
  // Measured wrapper: `style`-only (no className) so RN-Web fires onLayout.
552
511
  // `rootStyle` is placement-aware — a `maxHeight` cap that gives the
@@ -559,12 +518,15 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
559
518
  root (the CSS flexbox `min-height:auto` trap; a no-op on native,
560
519
  where Yoga already defaults min-height to 0). */}
561
520
  <View className="flex-1 min-h-0 bg-black">
562
- {/* Photo grid (renders behind the translucent header). */}
521
+ {/* Photo grid full-bleed black canvas CONTENT under the shared
522
+ Dialog nav bar (the Dialog reserves the bar's height above this
523
+ and its gradient fades a little further down; the top padding
524
+ clears that fade). */}
563
525
  {isEmpty && loadError ? (
564
526
  /* Terminal load failure — a DISTINCT error surface (not the
565
- empty state), pushed below the translucent header so Cancel
566
- stays reachable. Retry re-runs the list query. */
567
- <View className="flex-1 items-center justify-center pt-[60px]">
527
+ empty state), centered in the immersive frame. Retry re-runs
528
+ the list query. */
529
+ <View className="flex-1 items-center justify-center">
568
530
  <FileLibraryError
569
531
  title={t('fileManagement.loadError.title')}
570
532
  description={t('fileManagement.loadError.description')}
@@ -579,14 +541,11 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
579
541
  ) : isEmpty && loading ? (
580
542
  /* Loading skeleton — the picker's OWN shape: shimmer tiles laid
581
543
  out with the SAME placement-aware geometry the real grid uses
582
- (`computePhotoGridLayout(effectiveWidth)`), pushed below the
583
- translucent header by the grid content padding. `flexWrap`
584
- breaks rows exactly at `columns` because the last tile in each
585
- row carries no right margin (a full row + gutters fits the
586
- measured width by construction). The real translucent header
587
- still floats on top, so Cancel stays reachable while loading. */
544
+ (`computePhotoGridLayout(effectiveWidth)`), edge-to-edge under
545
+ the floating bar. `flexWrap` breaks rows exactly at `columns`
546
+ because the last tile in each row carries no right margin (a
547
+ full row + gutters fits the measured width by construction). */
588
548
  <View
589
- className="pt-[60px]"
590
549
  style={{ flexDirection: 'row', flexWrap: 'wrap' }}
591
550
  pointerEvents="none"
592
551
  accessibilityElementsHidden
@@ -594,7 +553,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
594
553
  >
595
554
  {Array.from({ length: skeletonTileCount }, (_, index) => (
596
555
  <Skeleton.Box
597
- key={index}
556
+ key={`skeleton-${index}`}
598
557
  width={cellSize}
599
558
  height={cellSize}
600
559
  borderRadius={CELL_CORNER_RADIUS}
@@ -607,7 +566,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
607
566
  ))}
608
567
  </View>
609
568
  ) : isEmpty ? (
610
- <View className="flex-1 items-center justify-center px-space-32 pt-[60px]">
569
+ <View className="flex-1 items-center justify-center px-space-32">
611
570
  <View className="opacity-30 mb-space-16">
612
571
  <MaterialCommunityIcons name="image-outline" size={64} color="#FFFFFF" />
613
572
  </View>
@@ -648,7 +607,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
648
607
  keyExtractor={keyExtractor}
649
608
  numColumns={columns}
650
609
  className="flex-1 min-h-0"
651
- contentContainerClassName="pt-[60px] pb-space-24"
610
+ contentContainerClassName="pb-space-24"
652
611
  showsVerticalScrollIndicator={false}
653
612
  refreshControl={
654
613
  <RefreshControl
@@ -656,7 +615,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
656
615
  onRefresh={onRefresh}
657
616
  tintColor="#FFFFFF"
658
617
  colors={[primaryColor]}
659
- progressViewOffset={GRID_CONTENT_PADDING_TOP}
618
+ progressViewOffset={FLOATING_NAV_BAR_HEIGHT}
660
619
  />
661
620
  }
662
621
  onEndReached={handleEndReached}
@@ -667,97 +626,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
667
626
  windowSize={9}
668
627
  />
669
628
  )}
670
-
671
- {/* Translucent black header floating over the grid. The Dialog owns
672
- the drag handle (a floating pill above this bar in bottom
673
- placement, drawn at a higher z-index; none in center), so the
674
- header no longer reserves the 28dp handle strip — it is just the
675
- 56dp app bar (`min-h-[56px]`). */}
676
- <View className="absolute top-0 left-0 right-0 flex-row items-center justify-between px-space-12 z-30 bg-[#000000EB] min-h-[56px]">
677
- <View className="flex-row items-center justify-between w-full h-14">
678
- <View className="basis-0 grow flex-row items-center justify-start">
679
- <TouchableOpacity
680
- onPress={onCancel}
681
- className="px-1.5 py-space-8 min-h-9 min-w-11 justify-center"
682
- accessibilityRole="button"
683
- accessibilityLabel={t('fileManagement.a11y.cancelPicker')}
684
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
685
- >
686
- <Text className="text-white text-[17px] font-medium">
687
- {t('fileManagement.cancel')}
688
- </Text>
689
- </TouchableOpacity>
690
- </View>
691
- <View pointerEvents="none">
692
- <Text
693
- className="text-white text-[17px] font-semibold text-center"
694
- style={{ letterSpacing: -0.2 }}
695
- numberOfLines={1}
696
- >
697
- {t('fileManagement.choosePhoto')}
698
- </Text>
699
- </View>
700
- <View className="basis-0 grow flex-row items-center justify-end">
701
- {multiSelect ? (
702
- <TouchableOpacity
703
- onPress={handleConfirm}
704
- disabled={confirmDisabled}
705
- className="flex-row items-center gap-1.5 px-3.5 py-space-8 rounded-full min-h-9"
706
- style={{
707
- backgroundColor: confirmDisabled
708
- ? 'rgba(255,255,255,0.18)'
709
- : primaryColor,
710
- opacity: confirmDisabled ? 0.6 : 1,
711
- }}
712
- accessibilityRole="button"
713
- accessibilityLabel={t('fileManagement.a11y.confirmSelection')}
714
- accessibilityState={{ disabled: confirmDisabled }}
715
- >
716
- <Text className="text-white text-[15px] font-semibold">
717
- {confirmLabel}
718
- </Text>
719
- </TouchableOpacity>
720
- ) : (
721
- isOwner && allowUpload && (
722
- <TouchableOpacity
723
- onPress={onUpload}
724
- disabled={uploading || isPickingDocument}
725
- className={`flex-row items-center gap-1.5 py-space-8 rounded-full min-h-9${showUploadLabel ? ' px-3.5' : ' px-space-8 w-9 justify-center'}`}
726
- style={{ backgroundColor: primaryColor }}
727
- accessibilityRole="button"
728
- accessibilityLabel={t('fileManagement.a11y.uploadFromDevice')}
729
- accessibilityState={{ busy: uploading || isPickingDocument }}
730
- >
731
- {(uploading || isPickingDocument) ? (
732
- <ActivityIndicator size="small" color="#FFFFFF" />
733
- ) : (
734
- <>
735
- <Ionicons name="cloud-upload" size={16} color="#FFFFFF" />
736
- {showUploadLabel && (
737
- <Text className="text-white text-[15px] font-semibold">
738
- {t('fileManagement.upload')}
739
- </Text>
740
- )}
741
- </>
742
- )}
743
- </TouchableOpacity>
744
- )
745
- )}
746
- </View>
747
- </View>
748
- {/* Subtle top progress bar during upload (non-blocking). */}
749
- {uploading && (
750
- <View className="absolute left-0 right-0 bottom-0 h-0.5 bg-[#FFFFFF1F]">
751
- <View
752
- className="h-full"
753
- style={{
754
- width: `${Math.round(progressFraction * 100)}%`,
755
- backgroundColor: primaryColor,
756
- }}
757
- />
758
- </View>
759
- )}
760
- </View>
761
629
  </View>
762
630
  </View>
763
631
  );