@oxyhq/services 22.11.1 → 22.12.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 (66) hide show
  1. package/lib/commonjs/ui/hooks/useSurfaceHeader.js +7 -5
  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 +55 -13
  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 +43 -153
  16. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  17. package/lib/module/ui/hooks/useSurfaceHeader.js +7 -5
  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 +55 -13
  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 +43 -153
  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.map +1 -1
  39. package/lib/typescript/commonjs/ui/screens/PaymentGatewayScreen.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/ui/screens/WelcomeNewUserScreen.d.ts +2 -1
  42. package/lib/typescript/commonjs/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +0 -6
  44. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  45. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts +7 -5
  46. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
  47. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +8 -9
  48. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  49. package/lib/typescript/module/ui/screens/AvatarCropScreen.d.ts.map +1 -1
  50. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  51. package/lib/typescript/module/ui/screens/PaymentGatewayScreen.d.ts.map +1 -1
  52. package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
  53. package/lib/typescript/module/ui/screens/WelcomeNewUserScreen.d.ts +2 -1
  54. package/lib/typescript/module/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  55. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +0 -6
  56. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  57. package/package.json +4 -4
  58. package/src/ui/hooks/useSurfaceHeader.ts +25 -5
  59. package/src/ui/navigation/__tests__/getSurfaceConfig.test.ts +16 -2
  60. package/src/ui/navigation/surfaceRegistry.ts +14 -42
  61. package/src/ui/screens/AvatarCropScreen.tsx +14 -23
  62. package/src/ui/screens/FileManagementScreen.tsx +69 -25
  63. package/src/ui/screens/PaymentGatewayScreen.tsx +11 -0
  64. package/src/ui/screens/ProfileScreen.tsx +96 -111
  65. package/src/ui/screens/WelcomeNewUserScreen.tsx +23 -63
  66. package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +47 -170
@@ -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,27 @@ 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 (bottom-sheet on
78
+ * narrow, centered card on `md+`). The Dialog owns the shared gradient nav bar
79
+ * (Cancel / "Choose Photo" / Upload, wired by `FileManagementScreen` through
80
+ * `useSurfaceHeader`) the picker no longer paints a bar of its own. In this mode
81
+ * the Dialog reserves a fixed nav-bar-height SPACER above the picker's content and
82
+ * its gradient overlay fades a little further into the content; the grid's own
83
+ * top padding just clears that fade so the first row reads cleanly. Two fixed
84
+ * layout constants mirror Bloom internals:
85
+ * - `DIALOG_NAV_BAR_SPACER` mirrors Bloom's `DIALOG_NAV_BAR_HEIGHT` (not exported
86
+ * from `@oxyhq/bloom/dialog`) — the spacer the Dialog inserts above header-mode
87
+ * `scrollable:false` content. The picker's own height is reduced by it so the
88
+ * spacer + grid together fill the `frameSize` morph target (and never overflow
89
+ * the panel's `maxHeightRatio: 0.9` cap, which would clip the last row).
90
+ * - `HEADER_GRADIENT_FADE` is the gradient tail that bleeds past the bar into the
91
+ * content; the grid's `pt-[24px]` (fade + 4dp breathing room) clears it. The
92
+ * `GRID_CONTENT_PADDING_TOP` constant MUST stay in sync with that class.
104
93
  */
105
- const APP_BAR_HEIGHT = 56;
106
- const HEADER_HEIGHT = APP_BAR_HEIGHT; // 56
107
- const GRID_CONTENT_PADDING_TOP = HEADER_HEIGHT + 4; // 60
94
+ const DIALOG_NAV_BAR_SPACER = 52;
95
+ const HEADER_GRADIENT_FADE = 20;
96
+ const GRID_CONTENT_PADDING_TOP = HEADER_GRADIENT_FADE + 4; // 24 — matches `pt-[24px]`
108
97
 
109
98
  /**
110
99
  * Props for the dedicated photo picker view. Used by FileManagementScreen
@@ -120,7 +109,6 @@ export interface PhotoPickerViewProps {
120
109
  refreshing: boolean;
121
110
  uploading: boolean;
122
111
  isPickingDocument: boolean;
123
- uploadProgress: { current: number; total: number } | null;
124
112
  hasMore: boolean;
125
113
  loadingMore: boolean;
126
114
  /**
@@ -143,8 +131,6 @@ export interface PhotoPickerViewProps {
143
131
  onUpload: () => void;
144
132
  onRefresh: () => void;
145
133
  onLoadMore: () => void;
146
- onCancel: () => void;
147
- onConfirm: () => void;
148
134
  t: (key: string, vars?: Record<string, string | number>) => string;
149
135
  }
150
136
 
@@ -362,7 +348,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
362
348
  refreshing,
363
349
  uploading,
364
350
  isPickingDocument,
365
- uploadProgress,
366
351
  hasMore,
367
352
  loadingMore,
368
353
  loading,
@@ -377,8 +362,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
377
362
  onUpload,
378
363
  onRefresh,
379
364
  onLoadMore,
380
- onCancel,
381
- onConfirm,
382
365
  t,
383
366
  }) => {
384
367
  // Grid sizing width. We PREFER the measured grid-container width (the
@@ -400,12 +383,18 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
400
383
 
401
384
  // The picker FILLS its panel: the surface morphs the container UP to an
402
385
  // 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`.
408
- const panelHeight = useMemo(() => Math.round(windowHeight * 0.9), [windowHeight]);
386
+ // and this root takes that SAME 0.9-of-viewport height MINUS the Dialog's
387
+ // nav-bar spacer (which the Dialog inserts ABOVE this content in header mode).
388
+ // spacer + root then exactly fill the morph target and stay within the panel's
389
+ // `maxHeightRatio: 0.9` cap without the subtraction the grid's last row would
390
+ // clip. A `flex: 1` root can't fill (no definite bound propagates through the
391
+ // sheet's flex chain at rest, where the panel is height:auto), so the fixed
392
+ // height is required; the FlatList scrolls within it. Keep the 0.9 ratio in
393
+ // sync with the picker's `frameSize` in `surfaceRegistry.ts`.
394
+ const panelHeight = useMemo(
395
+ () => Math.round(windowHeight * 0.9) - DIALOG_NAV_BAR_SPACER,
396
+ [windowHeight],
397
+ );
409
398
  const rootStyle = useMemo(() => ({ height: panelHeight }), [panelHeight]);
410
399
 
411
400
  const effectiveWidth = gridWidth > 0 ? gridWidth : windowWidth;
@@ -440,9 +429,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
440
429
 
441
430
  const hasAnySelection = selectedIds.size > 0;
442
431
 
443
- // Compact icon-only upload pill on narrow sheets; full pill otherwise.
444
- const showUploadLabel = effectiveWidth >= 360;
445
-
446
432
  const isEmpty = photos.length === 0;
447
433
  const atSelectionLimit =
448
434
  multiSelect && maxSelection != null && selectedIds.size >= maxSelection;
@@ -467,16 +453,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
467
453
  [onPreviewPhoto],
468
454
  );
469
455
 
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
456
  // FlatList renderItem: each cell knows its enterIndex (for stagger).
481
457
  const renderItem = useCallback(
482
458
  ({ item, index }: { item: FileMetadata; index: number }) => {
@@ -537,16 +513,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
537
513
  onLoadMore();
538
514
  }, [loadingMore, hasMore, onLoadMore]);
539
515
 
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
516
  return (
551
517
  // Measured wrapper: `style`-only (no className) so RN-Web fires onLayout.
552
518
  // `rootStyle` is placement-aware — a `maxHeight` cap that gives the
@@ -559,12 +525,15 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
559
525
  root (the CSS flexbox `min-height:auto` trap; a no-op on native,
560
526
  where Yoga already defaults min-height to 0). */}
561
527
  <View className="flex-1 min-h-0 bg-black">
562
- {/* Photo grid (renders behind the translucent header). */}
528
+ {/* Photo grid full-bleed black canvas CONTENT under the shared
529
+ Dialog nav bar (the Dialog reserves the bar's height above this
530
+ and its gradient fades a little further down; the top padding
531
+ clears that fade). */}
563
532
  {isEmpty && loadError ? (
564
533
  /* 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]">
534
+ empty state), cleared below the nav bar's gradient fade.
535
+ Retry re-runs the list query. */
536
+ <View className="flex-1 items-center justify-center pt-[24px]">
568
537
  <FileLibraryError
569
538
  title={t('fileManagement.loadError.title')}
570
539
  description={t('fileManagement.loadError.description')}
@@ -579,14 +548,13 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
579
548
  ) : isEmpty && loading ? (
580
549
  /* Loading skeleton — the picker's OWN shape: shimmer tiles laid
581
550
  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. */
551
+ (`computePhotoGridLayout(effectiveWidth)`), cleared below the
552
+ nav bar's gradient fade. `flexWrap` breaks rows exactly at
553
+ `columns` because the last tile in each row carries no right
554
+ margin (a full row + gutters fits the measured width by
555
+ construction). */
588
556
  <View
589
- className="pt-[60px]"
557
+ className="pt-[24px]"
590
558
  style={{ flexDirection: 'row', flexWrap: 'wrap' }}
591
559
  pointerEvents="none"
592
560
  accessibilityElementsHidden
@@ -594,7 +562,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
594
562
  >
595
563
  {Array.from({ length: skeletonTileCount }, (_, index) => (
596
564
  <Skeleton.Box
597
- key={index}
565
+ key={`skeleton-${index}`}
598
566
  width={cellSize}
599
567
  height={cellSize}
600
568
  borderRadius={CELL_CORNER_RADIUS}
@@ -607,7 +575,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
607
575
  ))}
608
576
  </View>
609
577
  ) : isEmpty ? (
610
- <View className="flex-1 items-center justify-center px-space-32 pt-[60px]">
578
+ <View className="flex-1 items-center justify-center px-space-32 pt-[24px]">
611
579
  <View className="opacity-30 mb-space-16">
612
580
  <MaterialCommunityIcons name="image-outline" size={64} color="#FFFFFF" />
613
581
  </View>
@@ -648,7 +616,7 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
648
616
  keyExtractor={keyExtractor}
649
617
  numColumns={columns}
650
618
  className="flex-1 min-h-0"
651
- contentContainerClassName="pt-[60px] pb-space-24"
619
+ contentContainerClassName="pt-[24px] pb-space-24"
652
620
  showsVerticalScrollIndicator={false}
653
621
  refreshControl={
654
622
  <RefreshControl
@@ -667,97 +635,6 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
667
635
  windowSize={9}
668
636
  />
669
637
  )}
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
638
  </View>
762
639
  </View>
763
640
  );