@oxyhq/services 22.12.1 → 22.12.2

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 (55) hide show
  1. package/lib/commonjs/ui/components/FollowButton.js +5 -8
  2. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  3. package/lib/commonjs/ui/navigation/surfaceNavStack.js +8 -7
  4. package/lib/commonjs/ui/navigation/surfaceNavStack.js.map +1 -1
  5. package/lib/commonjs/ui/screens/ChangeAvatarScreen.js +48 -42
  6. package/lib/commonjs/ui/screens/ChangeAvatarScreen.js.map +1 -1
  7. package/lib/commonjs/ui/screens/CreateAccountScreen.js +55 -98
  8. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
  9. package/lib/commonjs/ui/screens/FileManagementScreen.js +21 -8
  10. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  11. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +14 -6
  12. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  13. package/lib/module/ui/components/FollowButton.js +5 -8
  14. package/lib/module/ui/components/FollowButton.js.map +1 -1
  15. package/lib/module/ui/navigation/surfaceNavStack.js +8 -7
  16. package/lib/module/ui/navigation/surfaceNavStack.js.map +1 -1
  17. package/lib/module/ui/screens/ChangeAvatarScreen.js +48 -42
  18. package/lib/module/ui/screens/ChangeAvatarScreen.js.map +1 -1
  19. package/lib/module/ui/screens/CreateAccountScreen.js +58 -101
  20. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
  21. package/lib/module/ui/screens/FileManagementScreen.js +21 -8
  22. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  23. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +14 -6
  24. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  25. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts +7 -4
  27. package/lib/typescript/commonjs/ui/navigation/surfaceNavStack.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts +9 -5
  29. package/lib/typescript/commonjs/ui/screens/ChangeAvatarScreen.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +3 -1
  33. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/ui/types/fileManagement.d.ts +10 -0
  35. package/lib/typescript/commonjs/ui/types/fileManagement.d.ts.map +1 -1
  36. package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
  37. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts +7 -4
  38. package/lib/typescript/module/ui/navigation/surfaceNavStack.d.ts.map +1 -1
  39. package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts +9 -5
  40. package/lib/typescript/module/ui/screens/ChangeAvatarScreen.d.ts.map +1 -1
  41. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  42. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  43. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +3 -1
  44. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  45. package/lib/typescript/module/ui/types/fileManagement.d.ts +10 -0
  46. package/lib/typescript/module/ui/types/fileManagement.d.ts.map +1 -1
  47. package/package.json +4 -4
  48. package/src/ui/components/FollowButton.tsx +5 -8
  49. package/src/ui/navigation/__tests__/surfaceNavStack.test.ts +49 -17
  50. package/src/ui/navigation/surfaceNavStack.ts +15 -11
  51. package/src/ui/screens/ChangeAvatarScreen.tsx +46 -39
  52. package/src/ui/screens/CreateAccountScreen.tsx +123 -167
  53. package/src/ui/screens/FileManagementScreen.tsx +21 -8
  54. package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +16 -6
  55. package/src/ui/types/fileManagement.ts +10 -0
@@ -1 +1 @@
1
- {"version":3,"file":"CreateAccountScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/CreateAccountScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAU/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAgF3D;;;;;GAKG;AACH,QAAA,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CA+SlD,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"CreateAccountScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/CreateAccountScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAM/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAiF3D;;;;;GAKG;AACH,QAAA,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAsQlD,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"FileManagementScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/FileManagementScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAe/B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAAyB,CAAC;AAiEzE,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CA4yC7D,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"FileManagementScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/FileManagementScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAe/B,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAAyB,CAAC;AAiEzE,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAyzC7D,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -16,6 +16,8 @@ export interface UseFileUploadStateParams {
16
16
  multiSelect: boolean;
17
17
  afterSelect: 'close' | 'back' | 'none';
18
18
  onSelect?: (file: FileMetadata) => void;
19
+ /** Forward-navigation picker: see {@link FileManagementScreenProps.onPicked}. */
20
+ onPicked?: (file: FileMetadata) => void;
19
21
  goBack?: () => void;
20
22
  onClose?: () => void;
21
23
  selectedIds: Set<string>;
@@ -42,6 +44,6 @@ export interface UseFileUploadStateResult {
42
44
  * orchestrator simply threads in the values these handlers depend on and
43
45
  * consumes the returned state/handlers.
44
46
  */
45
- export declare const useFileUploadState: ({ targetUserId, uploadFileMutation, defaultVisibility, selectMode, multiSelect, afterSelect, onSelect, goBack, onClose, selectedIds, setSelectedIds, t, }: UseFileUploadStateParams) => UseFileUploadStateResult;
47
+ export declare const useFileUploadState: ({ targetUserId, uploadFileMutation, defaultVisibility, selectMode, multiSelect, afterSelect, onSelect, onPicked, goBack, onClose, selectedIds, setSelectedIds, t, }: UseFileUploadStateParams) => UseFileUploadStateResult;
46
48
  export default useFileUploadState;
47
49
  //# sourceMappingURL=useFileUploadState.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useFileUploadState.d.ts","sourceRoot":"","sources":["../../../../../../../src/ui/screens/fileManagement/hooks/useFileUploadState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iDAA8C,CAAC;AAQlF,OAAO,EAEH,KAAK,iBAAiB,EAQzB,MAAM,cAAW,CAAC;AAEnB,qDAAqD;AACrD,MAAM,WAAW,2BAA2B;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACjB;AAsBD,8EAA8E;AAC9E,MAAM,WAAW,wBAAwB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;IACrD,iBAAiB,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;IACrD,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IACvC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,cAAc,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAClE,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC;CACtE;AAED,mDAAmD;AACnD,MAAM,WAAW,wBAAwB;IACrC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,YAAY,EAAE,iBAAiB,EAAE,CAAC;IAClC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,uEAAuE;IACvE,SAAS,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,cAAc,EAAE,2BAA2B,GAAG,IAAI,CAAC;IACnD,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,mBAAmB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,iBAAiB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3C;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,2JAahC,wBAAwB,KAAG,wBA+Y7B,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"useFileUploadState.d.ts","sourceRoot":"","sources":["../../../../../../../src/ui/screens/fileManagement/hooks/useFileUploadState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iDAA8C,CAAC;AAQlF,OAAO,EAEH,KAAK,iBAAiB,EAQzB,MAAM,cAAW,CAAC;AAEnB,qDAAqD;AACrD,MAAM,WAAW,2BAA2B;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACjB;AAsBD,8EAA8E;AAC9E,MAAM,WAAW,wBAAwB;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;IACrD,iBAAiB,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;IACrD,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IACvC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC,iFAAiF;IACjF,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,cAAc,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAClE,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC;CACtE;AAED,mDAAmD;AACnD,MAAM,WAAW,wBAAwB;IACrC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,YAAY,EAAE,iBAAiB,EAAE,CAAC;IAClC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,uEAAuE;IACvE,SAAS,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,cAAc,EAAE,2BAA2B,GAAG,IAAI,CAAC;IACnD,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,mBAAmB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,iBAAiB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3C;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,qKAchC,wBAAwB,KAAG,wBAsZ7B,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
@@ -7,6 +7,16 @@ export interface FileManagementScreenProps extends BaseScreenProps {
7
7
  selectMode?: boolean;
8
8
  multiSelect?: boolean;
9
9
  onSelect?: (file: FileMetadata) => void;
10
+ /**
11
+ * Forward-navigation picker: called with the picked file in single-select
12
+ * mode when the picker is one FRAME of a larger in-surface flow. Unlike
13
+ * {@link onSelect}, the caller is expected to NAVIGATE the surface onward
14
+ * (e.g. the avatar flow pushes the cropper on top of this picker) — so on
15
+ * select the picker neither dismisses nor closes the session, and its
16
+ * Cancel/back pops THIS frame (returning to the frame that opened the picker)
17
+ * rather than closing. Takes precedence over `onSelect`/`dismiss` when set.
18
+ */
19
+ onPicked?: (file: FileMetadata) => void;
10
20
  onConfirmSelection?: OnConfirmFileSelection;
11
21
  initialSelectedIds?: string[];
12
22
  maxSelection?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"fileManagement.d.ts","sourceRoot":"","sources":["../../../../../src/ui/types/fileManagement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAc,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,MAAM,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;AAErE,MAAM,WAAW,yBAA0B,SAAQ,eAAe;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IAExC,kBAAkB,CAAC,EAAE,sBAAsB,CAAC;IAE5C,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IACxC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;IACtD;;;OAGG;IACH,WAAW,CAAC,EAAE;QACV,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACL;AAED,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;AAC3E,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AACvD,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,CAAC"}
1
+ {"version":3,"file":"fileManagement.d.ts","sourceRoot":"","sources":["../../../../../src/ui/types/fileManagement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAc,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,MAAM,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;AAErE,MAAM,WAAW,yBAA0B,SAAQ,eAAe;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IAExC,kBAAkB,CAAC,EAAE,sBAAsB,CAAC;IAE5C,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IACxC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;IACtD;;;OAGG;IACH,WAAW,CAAC,EAAE;QACV,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACL;AAED,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;AAC3E,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AACvD,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/services",
3
- "version": "22.12.1",
3
+ "version": "22.12.2",
4
4
  "description": "OxyHQ Expo/React Native SDK — UI components, screens, and native features",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -127,7 +127,7 @@
127
127
  },
128
128
  "dependencies": {
129
129
  "@oxyhq/contracts": "0.17.0",
130
- "@oxyhq/core": "^12.10.3",
130
+ "@oxyhq/core": "^12.10.4",
131
131
  "@simplewebauthn/browser": "^13.3.0",
132
132
  "color": "^4.2.3"
133
133
  },
@@ -136,7 +136,7 @@
136
136
  "@commitlint/cli": "^17.6.5",
137
137
  "@commitlint/config-conventional": "^17.6.5",
138
138
  "@expo/vector-icons": "^15.0.3",
139
- "@oxyhq/core": "12.10.3",
139
+ "@oxyhq/core": "12.10.4",
140
140
  "@react-native-async-storage/async-storage": "^2.0.0",
141
141
  "@react-native-community/netinfo": "^11.4.1",
142
142
  "@react-native/babel-preset": "^0.86.0",
@@ -176,7 +176,7 @@
176
176
  },
177
177
  "peerDependencies": {
178
178
  "@expo/vector-icons": "^15.0.3",
179
- "@oxyhq/bloom": "^0.50.0",
179
+ "@oxyhq/bloom": "^0.50.1",
180
180
  "@oxyhq/core": "^12.10.0",
181
181
  "@react-native-async-storage/async-storage": "^2.0.0",
182
182
  "@react-native-community/netinfo": "^11.4.1",
@@ -178,17 +178,14 @@ const FollowButtonMultiInner = memo(function FollowButtonMultiInner({
178
178
 
179
179
  const isLoading = isSubmitting || isAnyLoading;
180
180
 
181
- // Populate the store with each member's follow status once on mount and again
182
- // only when the SET of target users changes. `fetchAllStatuses` is referentially
183
- // stable (its useFollow useCallback deps are [canUsePrivateApi, userIds,
184
- // oxyServices]; `userIds` inside useFollow is memoized, and the outer wrapper
185
- // memoizes `multiUserIds`), so depending on it plus the stable joined-string
186
- // key cannot self-retrigger — `allFollowing`/loading are intentionally NOT in
181
+ // Populate the store with each member's follow status on mount and whenever
182
+ // the target set changes. `fetchAllStatuses` is recreated when `userIds`
183
+ // changes inside `useFollow`, so it is the sole effect dependency — no need
184
+ // for a joined-string key. `allFollowing`/loading are intentionally NOT in
187
185
  // the deps.
188
- const userIdsKey = useMemo(() => userIds.join(','), [userIds]);
189
186
  useEffect(() => {
190
187
  fetchAllStatuses?.();
191
- }, [userIdsKey, fetchAllStatuses]);
188
+ }, [fetchAllStatuses]);
192
189
 
193
190
  const handlePress = useCallback(async (event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
194
191
  if (preventParentActions && event?.preventDefault) {
@@ -45,8 +45,9 @@ describe('createSurfaceNavStack', () => {
45
45
  expect(stack.store.getState().closeResult).toBe('done');
46
46
  });
47
47
 
48
- describe('result-bearing sub-flow (morphed-in avatar picker)', () => {
48
+ describe('result-bearing sub-flow (morphed-in avatar flow)', () => {
49
49
  it('resolves with the descendant dismiss result and pops back to the caller frame', async () => {
50
+ // Device / camera path: ChangeAvatar navigates straight to the cropper.
50
51
  const stack = createSurfaceNavStack('EditProfile');
51
52
  const flow = stack.beginFlow('ChangeAvatar');
52
53
  expect(stack.getTop().route).toBe('ChangeAvatar');
@@ -84,39 +85,70 @@ describe('createSurfaceNavStack', () => {
84
85
  await expect(flow).resolves.toBeUndefined();
85
86
  });
86
87
 
87
- it('NESTS flows: the inner selector resolves first, then the outer avatar flow', async () => {
88
- // ChangeAvatar (outer) → "My Oxy files" FileManagement (inner)back → crop.
88
+ it('My Oxy files: the picker and cropper are FORWARD frames; the cropper confirm resolves the flow', async () => {
89
+ // EditProfile (flow) ChangeAvatar → FileManagement pickerAvatarCrop, all
90
+ // plain forward frames in the ONE avatar flow. Selecting a file forward-
91
+ // navigates to the cropper (it does NOT resolve the flow); the cropper's
92
+ // confirm resolves the flow and pops all the way back to EditProfile.
89
93
  const stack = createSurfaceNavStack('EditProfile');
90
- const avatarFlow = stack.beginFlow('ChangeAvatar');
91
- const fileFlow = stack.beginFlow('FileManagement');
94
+ const flow = stack.beginFlow('ChangeAvatar');
95
+ stack.navigate('FileManagement', { selectMode: true }); // "My Oxy files" picker
96
+ stack.navigate('AvatarCrop', { imageFileId: 'file-1' }); // forward-nav on pick
97
+ stack.resolveFlowOrDismiss({ uri: 'file:///cropped.jpg' }); // cropper confirm
98
+ await expect(flow).resolves.toEqual({ uri: 'file:///cropped.jpg' });
99
+ expect(stack.getTop().route).toBe('EditProfile');
100
+ expect(stack.store.getState().closing).toBe(false);
101
+ });
102
+
103
+ it('My Oxy files: Back unwinds cropper → picker → list, then cancels the flow', async () => {
104
+ // The bug this fixes: Back from the cropper must return to the PICKER (re-pick),
105
+ // not straight to the ChangeAvatar list.
106
+ const stack = createSurfaceNavStack('EditProfile');
107
+ const flow = stack.beginFlow('ChangeAvatar');
108
+ stack.navigate('FileManagement', { selectMode: true });
109
+ stack.navigate('AvatarCrop', { imageFileId: 'file-1' });
110
+ expect(stack.goBack()).toBe(true); // crop -> picker (re-pick)
111
+ expect(stack.getTop().route).toBe('FileManagement');
112
+ expect(stack.goBack()).toBe(true); // picker -> ChangeAvatar
113
+ expect(stack.getTop().route).toBe('ChangeAvatar');
114
+ expect(stack.goBack()).toBe(true); // ChangeAvatar -> EditProfile (cancels flow)
115
+ await expect(flow).resolves.toBeUndefined();
116
+ expect(stack.getTop().route).toBe('EditProfile');
117
+ expect(stack.store.getState().closing).toBe(false);
118
+ });
119
+
120
+ // The stack still supports NESTED flows generically, even though no current
121
+ // caller opens a flow inside another (the avatar flow uses one flow + forward
122
+ // frames). These guard that capability.
123
+ it('supports NESTED flows: the inner flow resolves first, then the outer one', async () => {
124
+ const stack = createSurfaceNavStack('EditProfile');
125
+ const outer = stack.beginFlow('ChangeAvatar');
126
+ const inner = stack.beginFlow('FileManagement');
92
127
  expect(stack.getTop().route).toBe('FileManagement');
93
- // Pick a file → inner flow resolves, pops back to ChangeAvatar (outer still open).
94
128
  stack.resolveFlowOrDismiss({ id: 'file-1' });
95
- await expect(fileFlow).resolves.toEqual({ id: 'file-1' });
129
+ await expect(inner).resolves.toEqual({ id: 'file-1' });
96
130
  expect(stack.getTop().route).toBe('ChangeAvatar');
97
131
  expect(stack.store.getState().closing).toBe(false);
98
- // Now drill to crop and confirm → outer flow resolves, pops to EditProfile.
99
132
  stack.navigate('AvatarCrop');
100
133
  stack.resolveFlowOrDismiss({ uri: 'file:///c.jpg' });
101
- await expect(avatarFlow).resolves.toEqual({ uri: 'file:///c.jpg' });
134
+ await expect(outer).resolves.toEqual({ uri: 'file:///c.jpg' });
102
135
  expect(stack.getTop().route).toBe('EditProfile');
103
136
  });
104
137
 
105
- it('cancelling the inner selector returns to the outer flow, leaving it open', async () => {
138
+ it('backing out of a NESTED inner flow leaves the outer flow pending', async () => {
106
139
  const stack = createSurfaceNavStack('EditProfile');
107
- const avatarFlow = stack.beginFlow('ChangeAvatar');
108
- const fileFlow = stack.beginFlow('FileManagement');
109
- expect(stack.goBack()).toBe(true); // FileManagement -> ChangeAvatar (cancels inner)
110
- await expect(fileFlow).resolves.toBeUndefined();
140
+ const outer = stack.beginFlow('ChangeAvatar');
141
+ const inner = stack.beginFlow('FileManagement');
142
+ expect(stack.goBack()).toBe(true); // inner entry backed out -> cancels inner only
143
+ await expect(inner).resolves.toBeUndefined();
111
144
  expect(stack.getTop().route).toBe('ChangeAvatar');
112
- // The outer avatar flow is still pending.
113
145
  let outerSettled = false;
114
- void avatarFlow.then(() => { outerSettled = true; });
146
+ void outer.then(() => { outerSettled = true; });
115
147
  await Promise.resolve();
116
148
  expect(outerSettled).toBe(false);
117
149
  });
118
150
 
119
- it('tearing down the surface abandons ALL nested flows (undefined)', async () => {
151
+ it('tearing down the surface abandons ALL pending flows (undefined)', async () => {
120
152
  const stack = createSurfaceNavStack('EditProfile');
121
153
  const outer = stack.beginFlow('ChangeAvatar');
122
154
  const inner = stack.beginFlow('FileManagement');
@@ -54,10 +54,13 @@ export interface SurfaceNavStack {
54
54
  * `dismiss(result)` resolves it with `result`, backing out of the flow's entry
55
55
  * frame resolves it with `undefined` (cancelled) — popping the surface back to
56
56
  * the frame that started the flow WITHOUT dismissing it. This is how a screen
57
- * already living inside a surface (EditProfile, ManageAccount, ChangeAvatar)
58
- * opens the avatar picker / media selector as a MORPH instead of a stacked
59
- * surface while still awaiting its result. Flows NEST (ChangeAvatar the "My
60
- * Oxy files" selector → back), maintained as a stack.
57
+ * already living inside a surface (EditProfile, ManageAccount) opens the avatar
58
+ * flow as a MORPH instead of a stacked surface while still awaiting its result.
59
+ * The avatar flow is ONE flow (`ChangeAvatar`) with plain forward frames pushed
60
+ * on top (the "My Oxy files" `FileManagement` picker, then `AvatarCrop`); the
61
+ * cropper's `dismiss(result)` resolves THIS flow. Flows are kept as a STACK so
62
+ * they can nest generically, even though no current caller opens a flow inside
63
+ * another.
61
64
  */
62
65
  beginFlow(route: RouteName, props?: Record<string, unknown>): Promise<unknown>;
63
66
  /**
@@ -94,13 +97,14 @@ export function createSurfaceNavStack(
94
97
  return frames[frames.length - 1];
95
98
  };
96
99
 
97
- // Result-bearing sub-flows currently running in this surface (the morphed-in
98
- // avatar picker, and NESTED within it the "My Oxy files" media selector).
99
- // A STACK: flows nest (ChangeAvatar FileManagement back to ChangeAvatar).
100
- // Held OUTSIDE the store (resolvers are functions; flow presence must not
101
- // re-render frames). Each flow's `returnLength` is the frame count BEFORE its
102
- // entry frame was pushed, so settling pops back to exactly the frame that
103
- // started it. `flowStack` is ordered outer → inner; the top is the innermost.
100
+ // Result-bearing sub-flows currently running in this surface (e.g. the morphed-
101
+ // in avatar flow, whose `ChangeAvatar FileManagement picker AvatarCrop`
102
+ // frames are plain forward pushes ON TOP of the ONE flow entry frame). Held as a
103
+ // STACK so flows can nest generically, and OUTSIDE the store (resolvers are
104
+ // functions; flow presence must not re-render frames). Each flow's `returnLength`
105
+ // is the frame count BEFORE its entry frame was pushed, so settling pops back to
106
+ // exactly the frame that started it. `flowStack` is ordered outer → inner; the
107
+ // top is the innermost.
104
108
  const flowStack: { returnLength: number; resolve: (result: unknown) => void }[] = [];
105
109
 
106
110
  const requestDismiss = (result?: unknown): void => {
@@ -4,11 +4,15 @@
4
4
  * Shows the current avatar large, then the four sources the user can pick from:
5
5
  * device gallery, camera, their existing Oxy files, and removing the current
6
6
  * photo. Every source that yields an image NAVIGATES WITHIN THIS SURFACE to
7
- * `AvatarCrop` (never presents it on top), so the panel morphs from this list's
8
- * size to the crop editor's and back Cancel on the cropper returns here to
9
- * re-pick. One container reshaping, not two sheets. The forward morph is
10
- * flash-free because only the top frame renders and the cropper does all its own
11
- * async work (URL resolution + measurement), so this list is never left on screen
7
+ * `AvatarCrop` (never presents it on top), so the panel morphs from frame to
8
+ * frame instead of stacking sheets. The device / camera sources navigate straight
9
+ * to the cropper; the "My Oxy files" source first navigates to the `FileManagement`
10
+ * picker frame, which then forward-navigates to the cropper on selection a real
11
+ * three-frame stack (this list picker cropper). So Cancel on the cropper
12
+ * returns to whichever frame opened it (the picker when re-picking from Oxy files,
13
+ * this list otherwise), and Cancel on the picker returns here. The forward morph
14
+ * is flash-free because only the top frame renders and the cropper does all its
15
+ * own async work (URL resolution + measurement), so no frame is left on screen
12
16
  * waiting.
13
17
  *
14
18
  * The surface resolves with an {@link AvatarCropResult} (the crop confirmed) or
@@ -37,10 +41,10 @@ import { surfaces as bloomSurfaces } from '@oxyhq/bloom/surfaces';
37
41
  import { useTheme } from '@oxyhq/bloom/theme';
38
42
  import { toast } from '@oxyhq/bloom';
39
43
  import { getAccountDisplayName, logger } from '@oxyhq/core';
44
+ import type { FileMetadata } from '@oxyhq/core';
40
45
  import { useOxy } from '../context/OxyContext';
41
46
  import { useI18n } from '../hooks/useI18n';
42
47
  import { useSurfaceHeader } from '../hooks/useSurfaceHeader';
43
- import { openWithinOrPresent } from '../navigation/surfaces';
44
48
  import { SettingsIcon } from '../components/SettingsIcon';
45
49
  import type { BaseScreenProps } from '../types/navigation';
46
50
 
@@ -229,44 +233,47 @@ const ChangeAvatarScreen: React.FC<BaseScreenProps> = ({ navigate, dismiss }) =>
229
233
  );
230
234
 
231
235
  /**
232
- * Pick from the files the user already has on Oxy. The image-only media
233
- * selector MORPHS into this surface (a nested sub-flow) like every other
234
- * screen and resolves with the chosen file; picking one then morphs on to the
235
- * crop editor. When triggered with no surface open it presents cold instead.
236
+ * Forward the file the user picked from their Oxy library on to the crop
237
+ * editor. Given to the `FileManagement` picker as its `onPicked` handler, so
238
+ * SELECTING a file pushes the cropper ON TOP of the picker frame the picker
239
+ * stays below, and Cancel on the cropper returns to it to re-pick.
240
+ *
241
+ * The file's ID is handed straight to the cropper WITHOUT resolving its
242
+ * (usually private) download URL here: the cropper resolves it and shows its
243
+ * own loading state on the dark canvas, so the picker never lingers for the
244
+ * network round-trip (the "flash back to the avatar screen" the resolve-here
245
+ * approach caused).
236
246
  */
237
- const pickFromOxyFiles = useCallback(async () => {
238
- if (busy) return;
239
- setBusy(true);
240
- try {
241
- const file = await openWithinOrPresent('FileManagement', {
242
- selectMode: true,
243
- multiSelect: false,
244
- disabledMimeTypes: NON_IMAGE_MIME_TYPES,
245
- });
246
- if (!file) return;
247
-
247
+ const handleOxyFilePicked = useCallback(
248
+ (file: FileMetadata) => {
249
+ // The image-only picker never surfaces non-images, but guard
250
+ // defensively — a non-image would break the cropper.
248
251
  if (!file.contentType?.startsWith('image/')) {
249
252
  toast.error(t('editProfile.toasts.selectImage'));
250
253
  return;
251
254
  }
252
-
253
- // Hand the file's ID straight to the cropper WITHOUT resolving its
254
- // (usually private) download URL here. The cropper resolves it — and
255
- // shows its own loading state on the dark canvas while it does — so
256
- // this source list is never left on screen for the network round-trip
257
- // that resolution takes (the "goes back to the avatar screen" flash).
258
255
  goToCrop({ imageFileId: file.id });
259
- } catch (err) {
260
- logger.error(
261
- 'Oxy files pick failed',
262
- err instanceof Error ? err : new Error(String(err)),
263
- { component: LOG_COMPONENT },
264
- );
265
- toast.error(t('changeAvatar.errors.loadImageFailed'));
266
- } finally {
267
- setBusy(false);
268
- }
269
- }, [busy, goToCrop, t]);
256
+ },
257
+ [goToCrop, t],
258
+ );
259
+
260
+ /**
261
+ * Pick from the files the user already has on Oxy. Navigates WITHIN this
262
+ * surface to the image-only `FileManagement` picker as a real frame (not a
263
+ * resolve-and-pop sub-flow): selecting a file forward-navigates to the crop
264
+ * editor via {@link handleOxyFilePicked}, giving a three-frame stack
265
+ * (ChangeAvatar → picker → crop). Back from the cropper returns to the picker
266
+ * to re-pick; back from the picker returns here. The cropper's confirm
267
+ * resolves the whole avatar flow with the crop result.
268
+ */
269
+ const pickFromOxyFiles = useCallback(() => {
270
+ navigate?.('FileManagement', {
271
+ selectMode: true,
272
+ multiSelect: false,
273
+ disabledMimeTypes: NON_IMAGE_MIME_TYPES,
274
+ onPicked: handleOxyFilePicked,
275
+ });
276
+ }, [navigate, handleOxyFilePicked]);
270
277
 
271
278
  /** Confirm, then resolve the surface with the removal so the caller writes it. */
272
279
  const removeCurrentPhoto = useCallback(async () => {
@@ -341,7 +348,7 @@ const ChangeAvatarScreen: React.FC<BaseScreenProps> = ({ navigate, dismiss }) =>
341
348
  title={t('changeAvatar.sources.files.title')}
342
349
  description={t('changeAvatar.sources.files.description')}
343
350
  disabled={busy}
344
- onPress={() => void pickFromOxyFiles()}
351
+ onPress={() => pickFromOxyFiles()}
345
352
  />
346
353
  <SettingsListItem
347
354
  icon={<SettingsIcon name="trash-can-outline" color={bloomTheme.colors.error} />}