@oxyhq/services 22.8.6 → 22.9.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 (152) hide show
  1. package/lib/commonjs/ui/components/OxyAccountDialog.js +30 -39
  2. package/lib/commonjs/ui/components/OxyAccountDialog.js.map +1 -1
  3. package/lib/commonjs/ui/components/OxyProvider.js +9 -29
  4. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  5. package/lib/commonjs/ui/components/SurfaceScreen.js +146 -0
  6. package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -0
  7. package/lib/commonjs/ui/components/fileManagement/UploadPreview.js +3 -3
  8. package/lib/commonjs/ui/components/fileManagement/UploadPreview.js.map +1 -1
  9. package/lib/commonjs/ui/context/OxyContext.js +32 -1
  10. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  11. package/lib/commonjs/ui/navigation/bottomSheetManager.js +38 -56
  12. package/lib/commonjs/ui/navigation/bottomSheetManager.js.map +1 -1
  13. package/lib/commonjs/ui/navigation/routes.js +7 -57
  14. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  15. package/lib/commonjs/ui/navigation/surfaceNavStack.js +98 -0
  16. package/lib/commonjs/ui/navigation/surfaceNavStack.js.map +1 -0
  17. package/lib/commonjs/ui/navigation/surfaceRegistry.js +108 -0
  18. package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -0
  19. package/lib/commonjs/ui/navigation/surfaces.js +219 -0
  20. package/lib/commonjs/ui/navigation/surfaces.js.map +1 -0
  21. package/lib/commonjs/ui/screens/FileManagementScreen.js +8 -1
  22. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +60 -3
  24. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  25. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +14 -17
  26. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  27. package/lib/commonjs/ui/screens/fileManagement/shared.js +13 -1
  28. package/lib/commonjs/ui/screens/fileManagement/shared.js.map +1 -1
  29. package/lib/commonjs/ui/types/surfaceScreen.js +6 -0
  30. package/lib/commonjs/ui/types/surfaceScreen.js.map +1 -0
  31. package/lib/module/ui/components/OxyAccountDialog.js +31 -40
  32. package/lib/module/ui/components/OxyAccountDialog.js.map +1 -1
  33. package/lib/module/ui/components/OxyProvider.js +10 -30
  34. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  35. package/lib/module/ui/components/SurfaceScreen.js +141 -0
  36. package/lib/module/ui/components/SurfaceScreen.js.map +1 -0
  37. package/lib/module/ui/components/fileManagement/UploadPreview.js +3 -3
  38. package/lib/module/ui/components/fileManagement/UploadPreview.js.map +1 -1
  39. package/lib/module/ui/context/OxyContext.js +32 -1
  40. package/lib/module/ui/context/OxyContext.js.map +1 -1
  41. package/lib/module/ui/navigation/bottomSheetManager.js +37 -53
  42. package/lib/module/ui/navigation/bottomSheetManager.js.map +1 -1
  43. package/lib/module/ui/navigation/routes.js +6 -56
  44. package/lib/module/ui/navigation/routes.js.map +1 -1
  45. package/lib/module/ui/navigation/surfaceNavStack.js +95 -0
  46. package/lib/module/ui/navigation/surfaceNavStack.js.map +1 -0
  47. package/lib/module/ui/navigation/surfaceRegistry.js +102 -0
  48. package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -0
  49. package/lib/module/ui/navigation/surfaces.js +207 -0
  50. package/lib/module/ui/navigation/surfaces.js.map +1 -0
  51. package/lib/module/ui/screens/FileManagementScreen.js +8 -1
  52. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  53. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +60 -3
  54. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  55. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +14 -18
  56. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  57. package/lib/module/ui/screens/fileManagement/shared.js +12 -0
  58. package/lib/module/ui/screens/fileManagement/shared.js.map +1 -1
  59. package/lib/module/ui/types/surfaceScreen.js +4 -0
  60. package/lib/module/ui/types/surfaceScreen.js.map +1 -0
  61. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts +29 -27
  62. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts +3 -2
  64. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts +24 -0
  66. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/ui/components/fileManagement/UploadPreview.d.ts +3 -10
  68. package/lib/typescript/commonjs/ui/components/fileManagement/UploadPreview.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts +24 -16
  71. package/lib/typescript/commonjs/ui/navigation/bottomSheetManager.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +2 -60
  73. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts +51 -0
  75. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +117 -0
  77. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts +79 -0
  79. package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts.map +1 -0
  80. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +7 -0
  82. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +1 -1
  84. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/ui/screens/fileManagement/shared.d.ts +4 -0
  86. package/lib/typescript/commonjs/ui/screens/fileManagement/shared.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/ui/types/surfaceScreen.d.ts +38 -0
  88. package/lib/typescript/commonjs/ui/types/surfaceScreen.d.ts.map +1 -0
  89. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts +29 -27
  90. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts.map +1 -1
  91. package/lib/typescript/module/ui/components/OxyProvider.d.ts +3 -2
  92. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  93. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts +24 -0
  94. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -0
  95. package/lib/typescript/module/ui/components/fileManagement/UploadPreview.d.ts +3 -10
  96. package/lib/typescript/module/ui/components/fileManagement/UploadPreview.d.ts.map +1 -1
  97. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  98. package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts +24 -16
  99. package/lib/typescript/module/ui/navigation/bottomSheetManager.d.ts.map +1 -1
  100. package/lib/typescript/module/ui/navigation/routes.d.ts +2 -60
  101. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  102. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts +51 -0
  103. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts.map +1 -0
  104. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +117 -0
  105. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -0
  106. package/lib/typescript/module/ui/navigation/surfaces.d.ts +79 -0
  107. package/lib/typescript/module/ui/navigation/surfaces.d.ts.map +1 -0
  108. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  109. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +7 -0
  110. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  111. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +1 -1
  112. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  113. package/lib/typescript/module/ui/screens/fileManagement/shared.d.ts +4 -0
  114. package/lib/typescript/module/ui/screens/fileManagement/shared.d.ts.map +1 -1
  115. package/lib/typescript/module/ui/types/surfaceScreen.d.ts +38 -0
  116. package/lib/typescript/module/ui/types/surfaceScreen.d.ts.map +1 -0
  117. package/package.json +2 -2
  118. package/src/ui/components/OxyAccountDialog.tsx +32 -38
  119. package/src/ui/components/OxyProvider.tsx +12 -34
  120. package/src/ui/components/SurfaceScreen.tsx +166 -0
  121. package/src/ui/components/fileManagement/UploadPreview.tsx +8 -16
  122. package/src/ui/context/OxyContext.tsx +27 -1
  123. package/src/ui/navigation/__tests__/getSurfaceConfig.test.ts +39 -0
  124. package/src/ui/navigation/bottomSheetManager.ts +46 -73
  125. package/src/ui/navigation/routes.ts +7 -106
  126. package/src/ui/navigation/surfaceNavStack.ts +110 -0
  127. package/src/ui/navigation/surfaceRegistry.ts +174 -0
  128. package/src/ui/navigation/surfaces.ts +244 -0
  129. package/src/ui/screens/FileManagementScreen.tsx +8 -1
  130. package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +69 -7
  131. package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +16 -18
  132. package/src/ui/screens/fileManagement/shared.ts +23 -0
  133. package/src/ui/types/surfaceScreen.ts +38 -0
  134. package/lib/commonjs/ui/components/BottomSheet.js +0 -49
  135. package/lib/commonjs/ui/components/BottomSheet.js.map +0 -1
  136. package/lib/commonjs/ui/components/BottomSheetRouter.js +0 -298
  137. package/lib/commonjs/ui/components/BottomSheetRouter.js.map +0 -1
  138. package/lib/module/ui/components/BottomSheet.js +0 -45
  139. package/lib/module/ui/components/BottomSheet.js.map +0 -1
  140. package/lib/module/ui/components/BottomSheetRouter.js +0 -292
  141. package/lib/module/ui/components/BottomSheetRouter.js.map +0 -1
  142. package/lib/typescript/commonjs/ui/components/BottomSheet.d.ts +0 -29
  143. package/lib/typescript/commonjs/ui/components/BottomSheet.d.ts.map +0 -1
  144. package/lib/typescript/commonjs/ui/components/BottomSheetRouter.d.ts +0 -9
  145. package/lib/typescript/commonjs/ui/components/BottomSheetRouter.d.ts.map +0 -1
  146. package/lib/typescript/module/ui/components/BottomSheet.d.ts +0 -29
  147. package/lib/typescript/module/ui/components/BottomSheet.d.ts.map +0 -1
  148. package/lib/typescript/module/ui/components/BottomSheetRouter.d.ts +0 -9
  149. package/lib/typescript/module/ui/components/BottomSheetRouter.d.ts.map +0 -1
  150. package/src/ui/components/BottomSheet.tsx +0 -48
  151. package/src/ui/components/BottomSheetRouter.tsx +0 -310
  152. package/src/ui/navigation/__tests__/getSheetConfig.test.ts +0 -36
@@ -0,0 +1,166 @@
1
+ import React, { useCallback, useEffect, useMemo, type ErrorInfo } from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import { useStore } from 'zustand';
4
+ import { useTheme } from '@oxyhq/bloom/theme';
5
+ import type { SurfaceControls } from '@oxyhq/bloom/surfaces';
6
+ import type { RouteName } from '../navigation/routes';
7
+ import { getScreenComponent } from '../navigation/routes';
8
+ import type { SurfaceNavStack } from '../navigation/surfaceNavStack';
9
+ import type { SurfacePresentation } from '../navigation/surfaceRegistry';
10
+ import {
11
+ closeAllRouteSurfaces,
12
+ navigateWithinOrPresent,
13
+ presentRoute,
14
+ replaceWithinOrPresent,
15
+ } from '../navigation/surfaces';
16
+ import type { BaseScreenProps } from '../types/navigation';
17
+
18
+ /** Error boundary catching screen render failures (e.g. a lazy `require()` throw). */
19
+ interface ScreenErrorBoundaryState {
20
+ error: Error | null;
21
+ }
22
+
23
+ class ScreenErrorBoundary extends React.Component<
24
+ { screenName: string; children: React.ReactNode },
25
+ ScreenErrorBoundaryState
26
+ > {
27
+ state: ScreenErrorBoundaryState = { error: null };
28
+
29
+ static getDerivedStateFromError(error: Error): ScreenErrorBoundaryState {
30
+ return { error };
31
+ }
32
+
33
+ componentDidCatch(error: Error, info: ErrorInfo): void {
34
+ if (__DEV__) {
35
+ console.error(
36
+ `[SurfaceScreen] Screen "${this.props.screenName}" crashed:`,
37
+ error,
38
+ info.componentStack,
39
+ );
40
+ }
41
+ }
42
+
43
+ componentDidUpdate(prevProps: { screenName: string }): void {
44
+ if (prevProps.screenName !== this.props.screenName && this.state.error) {
45
+ this.setState({ error: null });
46
+ }
47
+ }
48
+
49
+ render(): React.ReactNode {
50
+ if (this.state.error) {
51
+ return (
52
+ <View style={errorStyles.container}>
53
+ <Text style={errorStyles.title}>Something went wrong</Text>
54
+ {__DEV__ && <Text style={errorStyles.message}>{this.state.error.message}</Text>}
55
+ </View>
56
+ );
57
+ }
58
+ return this.props.children;
59
+ }
60
+ }
61
+
62
+ const errorStyles = StyleSheet.create({
63
+ container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
64
+ title: { fontSize: 16, fontWeight: '600', marginBottom: 8 },
65
+ message: { fontSize: 13, textAlign: 'center' },
66
+ });
67
+
68
+ export interface SurfaceScreenProps {
69
+ /** This surface's route history (the NAV-WITHIN axis). */
70
+ navStack: SurfaceNavStack;
71
+ /** Bloom's per-surface controls (dismiss / present a child) — the DEPTH axis. */
72
+ surface: SurfaceControls;
73
+ /** This surface's presentation, so `navigate` knows when to drill in vs. stack. */
74
+ presentation: SurfacePresentation;
75
+ }
76
+
77
+ /**
78
+ * Binds one SDK surface to its content: resolves the lazy screen for the current
79
+ * nav-stack frame and renders it inside the error boundary with the per-surface
80
+ * navigation props. Mounted by `surfaces.present` as the Bloom surface's content.
81
+ *
82
+ * Dismissal is driven through the nav stack's `closing` flag (watched in an
83
+ * effect) rather than a captured-controls call, so a dismiss requested before
84
+ * this surface mounted still resolves the Bloom surface exactly once.
85
+ */
86
+ function SurfaceScreen({ navStack, surface, presentation }: SurfaceScreenProps) {
87
+ const theme = useTheme();
88
+ const state = useStore(navStack.store);
89
+ const top = state.frames[state.frames.length - 1];
90
+
91
+ useEffect(() => {
92
+ if (state.closing) surface.dismiss(state.closeResult);
93
+ }, [state.closing, state.closeResult, surface]);
94
+
95
+ const ScreenComponent = useMemo(() => getScreenComponent(top.route) ?? null, [top.route]);
96
+
97
+ const navigate = useCallback(
98
+ (route: RouteName, props?: Record<string, unknown>) =>
99
+ navigateWithinOrPresent(presentation, navStack, route, props),
100
+ [navStack, presentation],
101
+ );
102
+
103
+ const replace = useCallback(
104
+ (route: RouteName, props?: Record<string, unknown>) =>
105
+ replaceWithinOrPresent(presentation, navStack, route, props),
106
+ [navStack, presentation],
107
+ );
108
+
109
+ const dismiss = useCallback((result?: unknown) => navStack.requestDismiss(result), [navStack]);
110
+
111
+ const canGoBack = useCallback(() => navStack.canGoBack(), [navStack]);
112
+
113
+ const setStep = useCallback((step: number) => navStack.setStep(step), [navStack]);
114
+
115
+ // Screen-facing back: pop a frame, else step a wizard back, else dismiss the
116
+ // surface — the exact behaviour the single `BottomSheetRouter` had per session.
117
+ const goBack = useCallback((): void => {
118
+ if (navStack.goBack()) return;
119
+ const current = navStack.getTop();
120
+ if (current.step > 0) {
121
+ navStack.setStep(current.step - 1);
122
+ return;
123
+ }
124
+ navStack.requestDismiss();
125
+ }, [navStack]);
126
+
127
+ const present = useCallback(
128
+ (route: RouteName, props?: Record<string, unknown>) => presentRoute(route, props).result,
129
+ [],
130
+ );
131
+
132
+ const screenProps = useMemo<BaseScreenProps>(() => {
133
+ const { initialStep: _omitInitialStep, ...rest } = top.props;
134
+ return {
135
+ // NAV-WITHIN
136
+ navigate,
137
+ goBack,
138
+ replace,
139
+ canGoBack,
140
+ initialStep: top.step,
141
+ step: top.step,
142
+ onStepChange: setStep,
143
+ setStep,
144
+ currentScreen: top.route,
145
+ // DEPTH — `onClose`/`onAuthenticated` unwind the whole session (matching the
146
+ // old `closeBottomSheet`); `dismiss`/`present` are the per-surface controls.
147
+ onClose: closeAllRouteSurfaces,
148
+ onAuthenticated: closeAllRouteSurfaces,
149
+ dismiss,
150
+ present,
151
+ // Theme + the route's own props.
152
+ theme: theme.mode,
153
+ ...rest,
154
+ };
155
+ }, [navigate, goBack, replace, canGoBack, setStep, present, dismiss, theme.mode, top.route, top.step, top.props]);
156
+
157
+ if (!ScreenComponent) return null;
158
+
159
+ return (
160
+ <ScreenErrorBoundary screenName={top.route}>
161
+ <ScreenComponent {...screenProps} />
162
+ </ScreenErrorBoundary>
163
+ );
164
+ }
165
+
166
+ export default SurfaceScreen;
@@ -4,23 +4,15 @@ import { Image as ExpoImage } from 'expo-image';
4
4
  import { Ionicons } from '@expo/vector-icons';
5
5
  import { Dialog, type DialogControlProps } from '@oxyhq/bloom';
6
6
  import { useTheme } from '@oxyhq/bloom/theme';
7
- import type { RNFileDescriptor } from '@oxyhq/core';
8
7
  import { formatFileSize, getFileIcon } from '../../utils/fileManagement';
9
-
10
- interface PendingFile {
11
- file: File | Blob | RNFileDescriptor;
12
- preview?: string;
13
- size: number;
14
- name: string;
15
- type: string;
16
- }
8
+ import type { PendingUploadFile } from '../../screens/fileManagement/shared';
17
9
 
18
10
  interface UploadPreviewProps {
19
11
  control?: DialogControlProps;
20
- pendingFiles: PendingFile[];
12
+ pendingFiles: PendingUploadFile[];
21
13
  onConfirm: () => void;
22
14
  onCancel: () => void;
23
- onRemoveFile: (index: number) => void;
15
+ onRemoveFile: (id: string) => void;
24
16
  inline?: boolean;
25
17
  }
26
18
 
@@ -34,10 +26,10 @@ const previewStyles = StyleSheet.create({
34
26
  });
35
27
 
36
28
  const UploadPreviewContent: React.FC<{
37
- pendingFiles: PendingFile[];
29
+ pendingFiles: PendingUploadFile[];
38
30
  onConfirm: () => void;
39
31
  onCancel: () => void;
40
- onRemoveFile: (index: number) => void;
32
+ onRemoveFile: (id: string) => void;
41
33
  showActions?: boolean;
42
34
  }> = ({
43
35
  pendingFiles,
@@ -61,11 +53,11 @@ const UploadPreviewContent: React.FC<{
61
53
  </View>
62
54
 
63
55
  <ScrollView className="flex-1 p-[16px]">
64
- {pendingFiles.map((pendingFile, index) => {
56
+ {pendingFiles.map((pendingFile) => {
65
57
  const isImage = pendingFile.type.startsWith('image/');
66
58
  return (
67
59
  <View
68
- key={`${pendingFile.name}-${pendingFile.size}-${index}`}
60
+ key={pendingFile.id}
69
61
  className="bg-secondary border-border flex-row items-center p-[12px] rounded-[12px] border mb-[12px] gap-[12px]"
70
62
  >
71
63
  {isImage && pendingFile.preview ? (
@@ -96,7 +88,7 @@ const UploadPreviewContent: React.FC<{
96
88
  </View>
97
89
  <TouchableOpacity
98
90
  className="p-[4px]"
99
- onPress={() => onRemoveFile(index)}
91
+ onPress={() => onRemoveFile(pendingFile.id)}
100
92
  >
101
93
  <Ionicons name="close-circle" size={24} color={colors.error} />
102
94
  </TouchableOpacity>
@@ -45,6 +45,7 @@ import { useDeviceManagement } from '../hooks/useDeviceManagement';
45
45
  import { getStorageKeys, createPlatformStorage, type StorageInterface } from '../utils/storageHelpers';
46
46
  import type { RouteName } from '../navigation/routes';
47
47
  import { showBottomSheet as globalShowBottomSheet } from '../navigation/bottomSheetManager';
48
+ import { presentDetached, type SurfaceInstance } from '../navigation/surfaces';
48
49
  import { useQueryClient, onlineManager } from '@tanstack/react-query';
49
50
  import { clearQueryCache } from '../hooks/queryClient';
50
51
  import { useAvatarPicker } from '../hooks/useAvatarPicker';
@@ -643,6 +644,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
643
644
  const commitSwitchedSessionRef = useRef(commitSwitchedSession);
644
645
  commitSwitchedSessionRef.current = commitSwitchedSession;
645
646
 
647
+ // The live AccountDialog surface (a stacked Bloom surface), while open. Held so
648
+ // `openAccountDialog` can no-op when already open and `closeAccountDialog` /
649
+ // `onSignedIn` can dismiss it. Cleared when the surface settles.
650
+ const accountDialogSurfaceRef = useRef<SurfaceInstance<'AccountDialog'> | null>(null);
651
+
646
652
  const accountDialogControllerRef = useRef<AccountDialogController | null>(null);
647
653
  if (!accountDialogControllerRef.current) {
648
654
  accountDialogControllerRef.current = createAccountDialogController({
@@ -655,7 +661,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
655
661
  socketFactory: io,
656
662
  commitSession: (session) => handleWebSessionRef.current(session),
657
663
  commitSwitchedSession: (session) => commitSwitchedSessionRef.current(session),
658
- onSignedIn: () => setAccountDialogOpen(false),
664
+ onSignedIn: () => {
665
+ accountDialogSurfaceRef.current?.dismiss();
666
+ setAccountDialogOpen(false);
667
+ },
659
668
  openUrl: (url) => {
660
669
  if (isWebBrowser()) {
661
670
  redirectToAuthorize(url);
@@ -680,11 +689,28 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
680
689
 
681
690
  const openAccountDialog = useCallback((view?: AccountDialogView): void => {
682
691
  accountDialogControllerRef.current?.setView(view ?? 'accounts');
692
+ // Present the AccountDialog surface on the shared Bloom stack the FIRST time
693
+ // (subsequent opens just re-point the controller's view above). `presentDetached`
694
+ // keeps it OUT of the `showBottomSheet` route-surface lineage, so closing the
695
+ // bottom-sheet session never touches the account dialog and vice-versa.
696
+ if (!accountDialogSurfaceRef.current) {
697
+ const instance = presentDetached(
698
+ 'AccountDialog',
699
+ { initialView: view ?? 'accounts' },
700
+ { placement: { base: 'bottom', md: 'center' }, dismissOnBackdrop: false, maxWidth: 420 },
701
+ );
702
+ accountDialogSurfaceRef.current = instance;
703
+ instance.result.finally(() => {
704
+ if (accountDialogSurfaceRef.current === instance) accountDialogSurfaceRef.current = null;
705
+ setAccountDialogOpen(false);
706
+ });
707
+ }
683
708
  setAccountDialogOpen(true);
684
709
  }, []);
685
710
 
686
711
  const closeAccountDialog = useCallback((): void => {
687
712
  accountDialogControllerRef.current?.cancelSignIn();
713
+ accountDialogSurfaceRef.current?.dismiss();
688
714
  setAccountDialogOpen(false);
689
715
  }, []);
690
716
 
@@ -0,0 +1,39 @@
1
+ import { getSurfaceConfig, getSurfacePresentation } from '../surfaceRegistry';
2
+
3
+ /**
4
+ * `getSurfaceConfig` resolves which Bloom surface a route is presented in. The
5
+ * flagship image picker (FileManagement in image-only select mode) upgrades to a
6
+ * full-bleed `'fullScreen'` surface that owns its own scrolling; every other
7
+ * route stays on the responsive `'sheet'`. These guard the routing contract.
8
+ */
9
+ describe('getSurfaceConfig presentation', () => {
10
+ const imageOnlyPickerProps = {
11
+ selectMode: true,
12
+ disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
13
+ };
14
+
15
+ it('defaults every route to the responsive sheet', () => {
16
+ expect(getSurfaceConfig('ManageAccount', {}).presentation).toBe('sheet');
17
+ expect(getSurfaceConfig('AvatarCrop', {}).presentation).toBe('sheet');
18
+ expect(getSurfacePresentation('ManageAccount', {})).toBe('sheet');
19
+ });
20
+
21
+ it('routes the FileManagement image-only picker to a full-screen surface', () => {
22
+ const config = getSurfaceConfig('FileManagement', imageOnlyPickerProps);
23
+ expect(config.presentation).toBe('fullScreen');
24
+ // The full-bleed picker owns its own FlatList → the host must not wrap it.
25
+ expect(config.scrollable).toBe(false);
26
+ });
27
+
28
+ it('keeps FileManagement on the sheet when it is not an image-only picker', () => {
29
+ // Browse mode (no selectMode) — the standard file manager.
30
+ expect(getSurfaceConfig('FileManagement', {}).presentation).toBe('sheet');
31
+ // Select mode that still allows non-image types is not the flagship picker.
32
+ expect(
33
+ getSurfaceConfig('FileManagement', {
34
+ selectMode: true,
35
+ disabledMimeTypes: ['video/'],
36
+ }).presentation,
37
+ ).toBe('sheet');
38
+ });
39
+ });
@@ -1,88 +1,61 @@
1
1
  import type { RouteName } from './routes';
2
2
  import { isValidRoute } from './routes';
3
- import { createStore } from 'zustand/vanilla';
3
+ import { getSurfacePresentation } from './surfaceRegistry';
4
+ import { closeAllRouteSurfaces, presentRoute, topRouteSurface } from './surfaces';
4
5
 
5
6
  /**
6
- * Bottom Sheet State Manager
7
+ * Bottom-sheet manager the INTERNAL adapter that keeps the historical
8
+ * `showBottomSheet` / `closeBottomSheet` API working on top of the new Bloom
9
+ * surface stack (`navigation/surfaces.ts`). The public signatures are unchanged;
10
+ * this file just re-expresses them as thin calls into the typed surface layer.
11
+ *
12
+ * The old single-surface store is gone: `showBottomSheet` now open-or-navigates
13
+ * the "base" (top-most) route surface, and cross-presentation targets (the
14
+ * full-bleed image picker) stack a NEW surface on top — Bloom coordinates the
15
+ * z-order / backdrop / dismiss so nothing clashes.
7
16
  */
8
17
 
9
- export interface BottomSheetState {
10
- currentScreen: RouteName | null;
11
- screenProps: Record<string, unknown>;
12
- currentStep?: number;
13
- history: Array<{ screen: RouteName; props: Record<string, unknown>; step?: number }>;
14
- isOpen: boolean;
15
- fullScreen: boolean;
16
- }
17
-
18
- const initialState: BottomSheetState = {
19
- currentScreen: null,
20
- screenProps: {},
21
- currentStep: undefined,
22
- history: [],
23
- isOpen: false,
24
- fullScreen: false,
25
- };
26
-
27
- export const bottomSheetStore = createStore<BottomSheetState>(() => initialState);
28
-
29
- export const getState = () => bottomSheetStore.getState();
30
-
18
+ /**
19
+ * Open a bottom-sheet route. If the top-most active surface hosts the SAME
20
+ * presentation (the common all-sheets case) the route is drilled into it; a route
21
+ * with a different presentation (the image picker's full-bleed surface) is
22
+ * presented as a NEW surface on top. Opening with no active surface presents the
23
+ * first one.
24
+ *
25
+ * `fullScreen` is accepted for signature compatibility and ignored — a route's
26
+ * surface is now derived from its registry config, not a per-call flag.
27
+ */
31
28
  export const showBottomSheet = (
32
- screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown>; fullScreen?: boolean },
29
+ screenOrConfig:
30
+ | RouteName
31
+ | { screen: RouteName; props?: Record<string, unknown>; fullScreen?: boolean },
33
32
  ): void => {
34
- const screen = typeof screenOrConfig === 'string' ? screenOrConfig : screenOrConfig.screen;
35
- const props = typeof screenOrConfig === 'string' ? {} : (screenOrConfig.props || {});
36
- const fullScreen = typeof screenOrConfig === 'string' ? false : (screenOrConfig.fullScreen ?? false);
37
-
38
- if (!isValidRoute(screen)) {
39
- if (__DEV__) console.warn(`[BottomSheet] Invalid route: ${screen}`);
40
- return;
41
- }
42
-
43
- const state = bottomSheetStore.getState();
44
-
45
- // Push current screen to history if navigating to different screen
46
- if (state.currentScreen && state.currentScreen !== screen) {
47
- bottomSheetStore.setState({
48
- history: [...state.history, {
49
- screen: state.currentScreen,
50
- props: state.screenProps,
51
- step: state.currentStep,
52
- }],
53
- });
54
- }
55
-
56
- bottomSheetStore.setState({
57
- currentScreen: screen,
58
- screenProps: props,
59
- currentStep: typeof props.initialStep === 'number' ? props.initialStep : undefined,
60
- isOpen: true,
61
- fullScreen,
62
- });
33
+ const screen = typeof screenOrConfig === 'string' ? screenOrConfig : screenOrConfig.screen;
34
+ const props = typeof screenOrConfig === 'string' ? {} : screenOrConfig.props ?? {};
35
+
36
+ if (!isValidRoute(screen)) {
37
+ if (__DEV__) console.warn(`[BottomSheet] Invalid route: ${screen}`);
38
+ return;
39
+ }
40
+
41
+ const top = topRouteSurface();
42
+ if (top && top.presentation === getSurfacePresentation(screen, props)) {
43
+ top.navigate(screen, props);
44
+ } else {
45
+ presentRoute(screen, props);
46
+ }
63
47
  };
64
48
 
49
+ /** Dismiss the whole bottom-sheet session (every active route surface). */
65
50
  export const closeBottomSheet = (): void => {
66
- bottomSheetStore.setState(initialState);
51
+ closeAllRouteSurfaces();
67
52
  };
68
53
 
54
+ /**
55
+ * Step back within the top-most surface's route history. Returns `true` when a
56
+ * frame was popped, `false` when the surface is already at its root frame.
57
+ */
69
58
  export const goBack = (): boolean => {
70
- const { history } = bottomSheetStore.getState();
71
-
72
- if (history.length > 0) {
73
- const prev = history[history.length - 1];
74
- bottomSheetStore.setState({
75
- currentScreen: prev.screen,
76
- screenProps: prev.props,
77
- currentStep: prev.step,
78
- history: history.slice(0, -1),
79
- });
80
- return true;
81
- }
82
-
83
- return false;
84
- };
85
-
86
- export const updateState = (updates: Partial<BottomSheetState>) => {
87
- bottomSheetStore.setState((state) => ({ ...state, ...updates }));
59
+ const top = topRouteSurface();
60
+ return top ? top.goBack() : false;
88
61
  };
@@ -1,4 +1,4 @@
1
- import type { ComponentType, ReactNode } from 'react';
1
+ import type { ComponentType } from 'react';
2
2
  import type { BaseScreenProps } from '../types/navigation';
3
3
 
4
4
  // Lazy loading: Screens are loaded on-demand to break require cycles
@@ -42,7 +42,8 @@ export type RouteName =
42
42
  | 'AvatarCrop' // Square-crop editor presented before avatar upload
43
43
  | 'Notifications' // Per-channel notification preferences
44
44
  | 'ConnectedApps' // OAuth-authorized third-party apps the user can revoke
45
- | 'Preferences'; // General user preferences (theme, reduce-motion, etc.)
45
+ | 'Preferences' // General user preferences (theme, reduce-motion, etc.)
46
+ | 'AccountDialog'; // Unified account switcher + sign-in surface (folded OxyAccountDialog body)
46
47
 
47
48
  // Lazy screen loaders - functions that return screen components on-demand
48
49
  // This breaks the require cycle by deferring imports until screens are actually needed
@@ -86,6 +87,10 @@ const screenLoaders: Record<RouteName, () => ComponentType<BaseScreenProps>> = {
86
87
  Notifications: () => require('../screens/NotificationsScreen').default,
87
88
  ConnectedApps: () => require('../screens/ConnectedAppsScreen').default,
88
89
  Preferences: () => require('../screens/PreferencesScreen').default,
90
+ // Unified account switcher + sign-in surface. Its body lives in the
91
+ // `OxyAccountDialog` component (folded from the standalone dialog); the
92
+ // surface stack provides the Dialog chrome around it.
93
+ AccountDialog: () => require('../components/OxyAccountDialog').default,
89
94
  };
90
95
 
91
96
  // Cache loaded components to avoid re-requiring
@@ -121,107 +126,3 @@ export const getScreenComponent = (routeName: RouteName): ComponentType<BaseScre
121
126
  export const isValidRoute = (routeName: string): routeName is RouteName => {
122
127
  return routeName in screenLoaders;
123
128
  };
124
-
125
- /**
126
- * Configuration that BottomSheetRouter applies to the underlying BottomSheet
127
- * for a given route. Adding new options here is additive — never rename or
128
- * remove fields without bumping the consumer surface as a breaking change.
129
- */
130
- export interface SheetRouteConfig {
131
- /**
132
- * Which surface hosts the route. `'sheet'` (default) renders the screen in
133
- * the in-tree `BottomSheet`. `'dialog'` renders it in a responsive Bloom
134
- * `<Dialog>` (bottom-sheet on narrow, centered card on `md+`) — used for the
135
- * flagship image picker, which reads better as a centered panel on wide
136
- * viewports than a full-width sheet. `BottomSheetRouter` keeps BOTH surfaces
137
- * mounted and routes the active screen into exactly one based on this value.
138
- */
139
- presentation: 'sheet' | 'dialog';
140
- /**
141
- * When `false`, BottomSheet skips its internal ScrollView and lets the
142
- * screen own scrolling. Required for screens that render a FlatList,
143
- * SectionList, or any other VirtualizedList — nesting one inside a
144
- * plain ScrollView breaks windowing and triggers a RN warning.
145
- *
146
- * Ignored for `presentation: 'dialog'` routes — those don't render in the
147
- * in-tree `BottomSheet` at all.
148
- */
149
- scrollable: boolean;
150
- /**
151
- * Controls the body-pan activation strategy on the underlying bloom
152
- * `BottomSheet`. `true` uses RNGH's `manualActivation` with scroll-handoff
153
- * (recommended for scrollable content — the only RNGH 2.x pattern that
154
- * doesn't steal vertical events from the inner scroller on Android).
155
- * `false` uses an always-active body pan that gates on scroll offset.
156
- *
157
- * Defaults to `true` for all routes — matches the historical in-tree
158
- * BottomSheet behavior. Per-route opt-out is possible if a screen needs
159
- * the always-active pan instead.
160
- */
161
- manualActivation: boolean;
162
- /**
163
- * When `true`, the backdrop dims proportionally with drag distance (iOS
164
- * Photos style). Defaults to `true` for all routes — matches the
165
- * historical in-tree BottomSheet behavior.
166
- */
167
- dynamicBackdrop: boolean;
168
- /**
169
- * Optional custom handle slot. When provided, replaces the default
170
- * 36×5 pill drag handle. The handle remains unconditionally draggable
171
- * via the dedicated handle gesture (when `manualActivation` is `true`).
172
- * Use sparingly — screens should default to the standard handle for
173
- * platform consistency.
174
- */
175
- handleComponent?: () => ReactNode;
176
- }
177
-
178
- /**
179
- * Predicate matching FileManagementScreen's internal `isImageOnlyPicker`
180
- * derivation. When the consumer restricts to image MIME types (no videos,
181
- * no audio, no documents), FileManagement renders the flagship PhotoPickerView
182
- * which owns its own FlatList. The sheet must therefore stop scrolling
183
- * children. Kept in sync with `FileManagementScreen` — both check the same
184
- * disabled MIME type families.
185
- */
186
- const isFileManagementImageOnlyPicker = (props: Record<string, unknown>): boolean => {
187
- if (!props.selectMode) return false;
188
- const disabled = props.disabledMimeTypes;
189
- if (!Array.isArray(disabled) || disabled.length === 0) return false;
190
- const has = (predicate: (mt: string) => boolean): boolean =>
191
- disabled.some((mt) => typeof mt === 'string' && predicate(mt));
192
- const blocksVideos = has((mt) => mt === 'video/' || mt.startsWith('video/'));
193
- const blocksAudio = has((mt) => mt === 'audio/' || mt.startsWith('audio/'));
194
- const blocksDocs = has(
195
- (mt) => mt === 'application/pdf' || mt === 'application/' || mt.startsWith('application/'),
196
- );
197
- return blocksVideos && blocksAudio && blocksDocs;
198
- };
199
-
200
- /** Defaults shared across all routes — preserves the historical in-tree BS UX. */
201
- const DEFAULT_SHEET_CONFIG: SheetRouteConfig = {
202
- presentation: 'sheet',
203
- scrollable: true,
204
- manualActivation: true,
205
- dynamicBackdrop: true,
206
- };
207
-
208
- /**
209
- * Returns the bottom-sheet configuration for a route. Defaults match the
210
- * pre-refactor in-tree `BottomSheet` (scrollable, manualActivation,
211
- * dynamicBackdrop). Routes opt out per-field as needed.
212
- */
213
- export const getSheetConfig = (
214
- routeName: RouteName | null,
215
- screenProps: Record<string, unknown>,
216
- ): SheetRouteConfig => {
217
- if (!routeName) return DEFAULT_SHEET_CONFIG;
218
- if (routeName === 'FileManagement' && isFileManagementImageOnlyPicker(screenProps)) {
219
- // The flagship photo picker renders in a responsive Bloom `<Dialog>`
220
- // (bottom-sheet on narrow, centered card on `md+`) rather than the
221
- // in-tree BottomSheet — it owns its own FlatList and reads better as a
222
- // centered panel on wide viewports. The Dialog surface, not this
223
- // config's `scrollable`, governs its scrolling.
224
- return { ...DEFAULT_SHEET_CONFIG, presentation: 'dialog' };
225
- }
226
- return DEFAULT_SHEET_CONFIG;
227
- };