@oxyhq/services 22.12.2 → 22.12.4

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 (77) hide show
  1. package/lib/commonjs/ui/components/oauthNavigation.js +11 -1
  2. package/lib/commonjs/ui/components/oauthNavigation.js.map +1 -1
  3. package/lib/commonjs/ui/hooks/useSurfaceHeader.js +44 -7
  4. package/lib/commonjs/ui/hooks/useSurfaceHeader.js.map +1 -1
  5. package/lib/commonjs/ui/navigation/surfaces.js +8 -3
  6. package/lib/commonjs/ui/navigation/surfaces.js.map +1 -1
  7. package/lib/commonjs/ui/screens/CreateAccountScreen.js +11 -1
  8. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
  9. package/lib/commonjs/ui/screens/FileManagementScreen.js +1 -0
  10. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  11. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +10 -7
  12. package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  13. package/lib/commonjs/ui/screens/fileManagement/UploadBar.js +19 -11
  14. package/lib/commonjs/ui/screens/fileManagement/UploadBar.js.map +1 -1
  15. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +41 -5
  16. package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  17. package/lib/commonjs/ui/utils/crossOriginRestore.js +5 -1
  18. package/lib/commonjs/ui/utils/crossOriginRestore.js.map +1 -1
  19. package/lib/commonjs/ui/utils/oauthReturn.js +5 -1
  20. package/lib/commonjs/ui/utils/oauthReturn.js.map +1 -1
  21. package/lib/module/ui/components/oauthNavigation.js +12 -2
  22. package/lib/module/ui/components/oauthNavigation.js.map +1 -1
  23. package/lib/module/ui/hooks/useSurfaceHeader.js +45 -8
  24. package/lib/module/ui/hooks/useSurfaceHeader.js.map +1 -1
  25. package/lib/module/ui/navigation/surfaces.js +7 -1
  26. package/lib/module/ui/navigation/surfaces.js.map +1 -1
  27. package/lib/module/ui/screens/CreateAccountScreen.js +12 -2
  28. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
  29. package/lib/module/ui/screens/FileManagementScreen.js +1 -0
  30. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  31. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +10 -7
  32. package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
  33. package/lib/module/ui/screens/fileManagement/UploadBar.js +20 -12
  34. package/lib/module/ui/screens/fileManagement/UploadBar.js.map +1 -1
  35. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +42 -6
  36. package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
  37. package/lib/module/ui/utils/crossOriginRestore.js +6 -2
  38. package/lib/module/ui/utils/crossOriginRestore.js.map +1 -1
  39. package/lib/module/ui/utils/oauthReturn.js +6 -2
  40. package/lib/module/ui/utils/oauthReturn.js.map +1 -1
  41. package/lib/typescript/commonjs/ui/components/oauthNavigation.d.ts +7 -0
  42. package/lib/typescript/commonjs/ui/components/oauthNavigation.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts +7 -0
  44. package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/ui/screens/fileManagement/UploadBar.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +3 -1
  51. package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/ui/utils/crossOriginRestore.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/ui/utils/oauthReturn.d.ts.map +1 -1
  54. package/lib/typescript/module/ui/components/oauthNavigation.d.ts +7 -0
  55. package/lib/typescript/module/ui/components/oauthNavigation.d.ts.map +1 -1
  56. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts +7 -0
  57. package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
  58. package/lib/typescript/module/ui/navigation/surfaces.d.ts.map +1 -1
  59. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  60. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  61. package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
  62. package/lib/typescript/module/ui/screens/fileManagement/UploadBar.d.ts.map +1 -1
  63. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +3 -1
  64. package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
  65. package/lib/typescript/module/ui/utils/crossOriginRestore.d.ts.map +1 -1
  66. package/lib/typescript/module/ui/utils/oauthReturn.d.ts.map +1 -1
  67. package/package.json +3 -3
  68. package/src/ui/components/oauthNavigation.ts +12 -2
  69. package/src/ui/hooks/useSurfaceHeader.ts +65 -8
  70. package/src/ui/navigation/surfaces.ts +9 -1
  71. package/src/ui/screens/CreateAccountScreen.tsx +13 -2
  72. package/src/ui/screens/FileManagementScreen.tsx +1 -0
  73. package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +4 -5
  74. package/src/ui/screens/fileManagement/UploadBar.tsx +17 -10
  75. package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +49 -6
  76. package/src/ui/utils/crossOriginRestore.ts +10 -1
  77. package/src/ui/utils/oauthReturn.ts +10 -1
@@ -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,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"}
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,mFAAmF;IACnF,UAAU,CAAC,EAAE,OAAO,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,iLAehC,wBAAwB,KAAG,wBA8b7B,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"crossOriginRestore.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/crossOriginRestore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAqB/C,8EAA8E;AAC9E,wBAAgB,2BAA2B,IAAI,OAAO,CAOrD;AAED,iFAAiF;AACjF,wBAAgB,+BAA+B,IAAI,IAAI,CAKtD;AAED,qEAAqE;AACrE,wBAAgB,6BAA6B,IAAI,IAAI,CAMpD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAIrE;AAED,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,yBAAyB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAiDlB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,gBAAgB,GAAG,kBAAkB,GAAG,IAAI,CAsBtF"}
1
+ {"version":3,"file":"crossOriginRestore.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/crossOriginRestore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAsB/C,8EAA8E;AAC9E,wBAAgB,2BAA2B,IAAI,OAAO,CAOrD;AAED,iFAAiF;AACjF,wBAAgB,+BAA+B,IAAI,IAAI,CAKtD;AAED,qEAAqE;AACrE,wBAAgB,6BAA6B,IAAI,IAAI,CAMpD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAIrE;AAED,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,yBAAyB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAiDlB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,gBAAgB,GAAG,kBAAkB,GAAG,IAAI,CA8BtF"}
@@ -1 +1 @@
1
- {"version":3,"file":"oauthReturn.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/oauthReturn.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAS/C,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3D;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE,GAAG,OAAO,CAAC,OAAO,CAAC,CA8DnB;AAeD;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAmB/C"}
1
+ {"version":3,"file":"oauthReturn.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/oauthReturn.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAU/C,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3D;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE,GAAG,OAAO,CAAC,OAAO,CAAC,CA8DnB;AAuBD;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAmB/C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/services",
3
- "version": "22.12.2",
3
+ "version": "22.12.4",
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.4",
130
+ "@oxyhq/core": "^12.10.5",
131
131
  "@simplewebauthn/browser": "^13.3.0",
132
132
  "color": "^4.2.3"
133
133
  },
@@ -176,7 +176,7 @@
176
176
  },
177
177
  "peerDependencies": {
178
178
  "@expo/vector-icons": "^15.0.3",
179
- "@oxyhq/bloom": "^0.50.1",
179
+ "@oxyhq/bloom": "^0.50.2",
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",
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { Linking } from 'react-native';
12
- import { logger } from '@oxyhq/core';
12
+ import { logger, persistOAuthReturnPath } from '@oxyhq/core';
13
13
 
14
14
  /** Minimal shape of the optional `expo-web-browser` auth-session result. */
15
15
  interface WebBrowserAuthResult {
@@ -37,9 +37,19 @@ export interface OpenAuthorizeResult {
37
37
  * Web: hand the TOP-LEVEL document to the OAuth authorize URL (a full-page
38
38
  * redirect, not a popup) so the RP returns to its registered `redirect_uri`.
39
39
  * No-op where `location` is unavailable (SSR / non-browser hosts).
40
+ *
41
+ * Records the current page first. `redirect_uri` is a registered apex origin
42
+ * (see `normalizeOAuthRedirectUri`), so the IdP always returns the tab to `/`,
43
+ * and the return handlers restore this path. Doing it here rather than at each
44
+ * call site is deliberate: every web authorize redirect — silent restore,
45
+ * interactive sign-in, the passkey hub — funnels through this function, so a
46
+ * new flow cannot forget to preserve the page the visitor came from.
40
47
  */
41
48
  export function redirectToAuthorize(url: string): void {
42
- (globalThis as { location?: Location }).location?.assign(url);
49
+ const location = (globalThis as { location?: Location }).location;
50
+ if (!location) return;
51
+ persistOAuthReturnPath(`${location.pathname}${location.search}${location.hash}`);
52
+ location.assign(url);
43
53
  }
44
54
 
45
55
  /**
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useLayoutEffect } from 'react';
1
+ import { createContext, useContext, useLayoutEffect, useRef } from 'react';
2
2
  import type { DialogHeaderConfig } from '@oxyhq/bloom/dialog';
3
3
 
4
4
  /**
@@ -42,21 +42,78 @@ interface SurfaceHeaderContextValue {
42
42
  */
43
43
  export const SurfaceHeaderContext = createContext<SurfaceHeaderContextValue | null>(null);
44
44
 
45
+ /**
46
+ * Shallow value-equality for a surface's header contribution.
47
+ *
48
+ * Scalars (`title`/`subtitle`/`largeTitle`/`tone`) compare by value; the object
49
+ * and node fields (`titleContent`/`left`/`right`/`primaryAction`/`actions`/
50
+ * `search`/`segments`/`progress`) compare by identity — callers memoize them, per
51
+ * this hook's contract. Function-typed affordances compare by PRESENCE, never
52
+ * identity: `onBack` legitimately gets a fresh closure each render, and its
53
+ * identity never changes what the header renders, so it must not count as a
54
+ * change (that is exactly what would drive the update loop).
55
+ */
56
+ function surfaceHeaderContentEqual(
57
+ a: SurfaceHeaderContent | null,
58
+ b: SurfaceHeaderContent | null,
59
+ ): boolean {
60
+ if (a === b) return true;
61
+ if (!a || !b) return false;
62
+ return (
63
+ a.title === b.title &&
64
+ a.titleContent === b.titleContent &&
65
+ a.subtitle === b.subtitle &&
66
+ a.largeTitle === b.largeTitle &&
67
+ a.left === b.left &&
68
+ a.right === b.right &&
69
+ !!a.onBack === !!b.onBack &&
70
+ a.primaryAction === b.primaryAction &&
71
+ a.actions === b.actions &&
72
+ a.search === b.search &&
73
+ a.segments === b.segments &&
74
+ a.tone === b.tone &&
75
+ a.progress === b.progress
76
+ );
77
+ }
78
+
45
79
  /**
46
80
  * Declare the Dialog nav header's content from within a surface screen — its
47
81
  * title/subtitle and any action slot. Merges over nothing (the host owns
48
82
  * back/close), replaces on change, and clears on unmount. Call it unconditionally;
49
83
  * it is a no-op outside a header-mode surface.
84
+ *
85
+ * Screens pass an INLINE `content` object (a fresh reference every render), so the
86
+ * push is guarded by {@link surfaceHeaderContentEqual}: the host's `setContent` is
87
+ * called only when the VALUE actually changes. Without that guard the setState
88
+ * would fire every render → the screen re-renders → the effect runs again → an
89
+ * infinite update loop (React error #185) in any consumer that does not memoize
90
+ * the object for us (i.e. anything not compiled by the React Compiler).
50
91
  */
51
92
  export function useSurfaceHeader(content: SurfaceHeaderContent | null | undefined): void {
52
93
  const ctx = useContext(SurfaceHeaderContext);
53
94
  const set = ctx?.setContent;
54
- // Set synchronously in the commit's layout phase so the bar/title fill in
55
- // BEFORE the browser paints — no first-frame flash of an empty bar.
95
+ const setRef = useRef(set);
96
+ setRef.current = set;
97
+ // The last value pushed to the host; `undefined` until the first push.
98
+ const lastRef = useRef<SurfaceHeaderContent | null | undefined>(undefined);
99
+
100
+ // Runs after EVERY commit (no deps array) so the bar/title fill in the same
101
+ // layout phase — but only pushes on a real value change, never on reference churn.
56
102
  useLayoutEffect(() => {
57
- if (!set) return;
58
- set(content ?? null);
59
- return () => set(null);
60
- // Callers must memoize `content` (especially slot nodes) so the header does not thrash.
61
- }, [set, content]);
103
+ const s = setRef.current;
104
+ if (!s) return;
105
+ const next = content ?? null;
106
+ if (lastRef.current !== undefined && surfaceHeaderContentEqual(lastRef.current, next)) return;
107
+ lastRef.current = next;
108
+ s(next);
109
+ });
110
+
111
+ // Clear this surface's contribution when the screen unmounts.
112
+ useLayoutEffect(
113
+ () => () => {
114
+ lastRef.current = undefined;
115
+ setRef.current?.(null);
116
+ },
117
+ [],
118
+ );
62
119
  }
@@ -5,7 +5,6 @@ import {
5
5
  type PresentOptions,
6
6
  type SurfaceControls,
7
7
  } from '@oxyhq/bloom/surfaces';
8
- import SurfaceScreen from '../components/SurfaceScreen';
9
8
  import type { RouteName } from './routes';
10
9
  import {
11
10
  getSurfaceConfig,
@@ -154,6 +153,15 @@ function presentInternal<K extends RouteName>(
154
153
  const config = getSurfaceConfig(route, props);
155
154
  const bloomOpts: PresentOptions = { label: route, ...bloomOptionsFor(config), ...opts };
156
155
 
156
+ // Lazy-resolved to break the surfaces.ts <-> SurfaceScreen module cycle: a
157
+ // mounted SurfaceScreen presents further surfaces (it imports these presenters),
158
+ // while presenting one renders a SurfaceScreen. Deferring the component to call
159
+ // time (both modules are initialized by then) makes the import graph one-way —
160
+ // the same idiom `routes.ts` uses to lazy-load screen components.
161
+ const SurfaceScreen = (
162
+ require('../components/SurfaceScreen') as typeof import('../components/SurfaceScreen')
163
+ ).default;
164
+
157
165
  const result = bloomSurfaces.present<SurfaceResult<K>>((surface: SurfaceControls) =>
158
166
  createElement(SurfaceScreen, {
159
167
  navStack,
@@ -1,5 +1,5 @@
1
1
  import type React from 'react';
2
- import { useState, useCallback, useRef } from 'react';
2
+ import { useState, useCallback, useRef, useEffect } from 'react';
3
3
  import { View, ActivityIndicator } from 'react-native';
4
4
  import { Ionicons } from '@expo/vector-icons';
5
5
  import type { AccountKind, CreateAccountInput, OrganizationCategory } from '@oxyhq/core';
@@ -118,6 +118,14 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
118
118
  const [isCreating, setIsCreating] = useState(false);
119
119
 
120
120
  const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
121
+ const usernameCheckSeqRef = useRef(0);
122
+
123
+ useEffect(() => () => {
124
+ usernameCheckSeqRef.current += 1;
125
+ if (debounceTimerRef.current) {
126
+ clearTimeout(debounceTimerRef.current);
127
+ }
128
+ }, []);
121
129
 
122
130
  // Debounced username availability check
123
131
  const checkUsername = useCallback((value: string) => {
@@ -146,9 +154,11 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
146
154
  setUsernameStatus('checking');
147
155
  setUsernameMessage('');
148
156
 
157
+ const seq = ++usernameCheckSeqRef.current;
149
158
  debounceTimerRef.current = setTimeout(async () => {
150
159
  try {
151
160
  const result = await oxyServices.checkUsernameAvailability(value);
161
+ if (seq !== usernameCheckSeqRef.current) return;
152
162
  setUsernameStatus(result.available ? 'available' : 'taken');
153
163
  setUsernameMessage(
154
164
  result.message
@@ -157,6 +167,7 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
157
167
  : (t('accounts.create.username.taken') || 'Username is taken')),
158
168
  );
159
169
  } catch {
170
+ if (seq !== usernameCheckSeqRef.current) return;
160
171
  setUsernameStatus('idle');
161
172
  setUsernameMessage(t('accounts.create.username.checkFailed') || 'Could not check availability');
162
173
  }
@@ -222,7 +233,7 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
222
233
  const title = t('accounts.create.title') || 'Create account';
223
234
 
224
235
  return (
225
- <View className="px-screen-margin pt-space-16 pb-space-32">
236
+ <View className="gap-space-16 px-screen-margin pt-space-16 pb-space-32">
226
237
 
227
238
  {/* Account type — canonical grouped selection rows (checkmark on the chosen one) */}
228
239
  <SettingsListGroup title={t('accounts.create.typeSection') || 'Account type'}>
@@ -437,6 +437,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
437
437
  onPicked,
438
438
  goBack,
439
439
  onClose,
440
+ imagesOnly: isImageOnlyPicker,
440
441
  selectedIds,
441
442
  setSelectedIds,
442
443
  t,
@@ -305,7 +305,6 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCe
305
305
  {isSelected && (
306
306
  <Animated.View
307
307
  ref={startRingPulse}
308
- pointerEvents="none"
309
308
  style={{
310
309
  position: 'absolute',
311
310
  top: 0,
@@ -316,14 +315,14 @@ const PhotoPickerCell = React.memo(function PhotoPickerCell(props: PhotoPickerCe
316
315
  borderWidth: SELECTION_RING_WIDTH,
317
316
  borderColor: primaryColor,
318
317
  transform: [{ scale: ringScale }],
318
+ pointerEvents: 'none',
319
319
  }}
320
320
  />
321
321
  )}
322
322
  {isSelected && (
323
323
  <View
324
- pointerEvents="none"
325
324
  className="absolute top-1.5 right-1.5 min-w-[22px] h-[22px] px-1.5 rounded-full items-center justify-center"
326
- style={{ backgroundColor: primaryColor }}
325
+ style={{ backgroundColor: primaryColor, pointerEvents: 'none' }}
327
326
  >
328
327
  <Text className="text-white text-[12px] font-bold leading-[14px]">
329
328
  {selectionIndex > 0 ? String(selectionIndex) : ''}
@@ -546,12 +545,12 @@ const PhotoPickerView: React.FC<PhotoPickerViewProps> = ({
546
545
  because the last tile in each row carries no right margin (a
547
546
  full row + gutters fits the measured width by construction). */
548
547
  <View
549
- style={{ flexDirection: 'row', flexWrap: 'wrap' }}
550
- pointerEvents="none"
548
+ style={{ flexDirection: 'row', flexWrap: 'wrap', pointerEvents: 'none' }}
551
549
  accessibilityElementsHidden
552
550
  importantForAccessibility="no-hide-descendants"
553
551
  >
554
552
  {Array.from({ length: skeletonTileCount }, (_, index) => (
553
+ // biome-ignore lint/suspicious/noArrayIndexKey: fixed-size skeleton grid, order never changes
555
554
  <Skeleton.Box
556
555
  key={`skeleton-${index}`}
557
556
  width={cellSize}
@@ -1,5 +1,5 @@
1
1
  import type React from 'react';
2
- import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
2
+ import { View, Text, ActivityIndicator, StyleSheet, Platform, type ViewStyle } from 'react-native';
3
3
  import { Ionicons } from '@expo/vector-icons';
4
4
 
5
5
  export interface UploadBarProps {
@@ -13,15 +13,19 @@ export interface UploadBarProps {
13
13
  }
14
14
 
15
15
  // The banner drop-shadow has no Bloom/Tailwind token equivalent, so it stays an
16
- // inline style (genuinely-required exception).
16
+ // inline style. `boxShadow` on web (RN-Web deprecated the `shadow*` props);
17
+ // RN `shadow*`/`elevation` on native.
17
18
  const shadowStyle = StyleSheet.create({
18
- banner: {
19
- shadowColor: '#000',
20
- shadowOpacity: 0.1,
21
- shadowRadius: 6,
22
- shadowOffset: { width: 0, height: 2 },
23
- elevation: 2,
24
- },
19
+ banner: Platform.select({
20
+ web: { boxShadow: '0px 2px 6px rgba(0, 0, 0, 0.1)' },
21
+ default: {
22
+ shadowColor: '#000',
23
+ shadowOpacity: 0.1,
24
+ shadowRadius: 6,
25
+ shadowOffset: { width: 0, height: 2 },
26
+ elevation: 2,
27
+ },
28
+ }) as ViewStyle,
25
29
  });
26
30
 
27
31
  /**
@@ -30,7 +34,10 @@ const shadowStyle = StyleSheet.create({
30
34
  * gates rendering on `!selectMode && uploading`.
31
35
  */
32
36
  const UploadBar: React.FC<UploadBarProps> = ({ uploadProgress, isDark, colors, t }) => (
33
- <View pointerEvents="none" className="absolute top-[72px] left-0 right-0 items-center z-50">
37
+ <View
38
+ className="absolute top-[72px] left-0 right-0 items-center z-50"
39
+ style={{ pointerEvents: 'none' }}
40
+ >
34
41
  <View
35
42
  className="flex-row items-center px-3.5 py-2.5 rounded-[24px] gap-2.5 border min-w-[200px]"
36
43
  style={[shadowStyle.banner, { backgroundColor: isDark ? '#222831EE' : '#FFFFFFEE', borderColor: colors.border }]}
@@ -1,5 +1,5 @@
1
1
  import type React from 'react';
2
- import { useCallback, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { useQueryClient } from '@tanstack/react-query';
4
4
  import { toast } from '@oxyhq/bloom';
5
5
  import type { AssetUploadInput, FileMetadata } from '@oxyhq/core';
@@ -62,6 +62,8 @@ export interface UseFileUploadStateParams {
62
62
  onPicked?: (file: FileMetadata) => void;
63
63
  goBack?: () => void;
64
64
  onClose?: () => void;
65
+ /** When true, the document picker and pre-upload validation accept images only. */
66
+ imagesOnly?: boolean;
65
67
  selectedIds: Set<string>;
66
68
  setSelectedIds: React.Dispatch<React.SetStateAction<Set<string>>>;
67
69
  t: (key: string, vars?: Record<string, string | number>) => string;
@@ -99,6 +101,7 @@ export const useFileUploadState = ({
99
101
  onPicked,
100
102
  goBack,
101
103
  onClose,
104
+ imagesOnly = false,
102
105
  selectedIds,
103
106
  setSelectedIds,
104
107
  t,
@@ -111,16 +114,45 @@ export const useFileUploadState = ({
111
114
 
112
115
  const queryClient = useQueryClient();
113
116
  const uploadStartRef = useRef<number | null>(null);
117
+ const postSelectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
118
+ const endUploadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
119
+ const isActiveRef = useRef(true);
120
+
121
+ const clearPostSelectTimer = useCallback(() => {
122
+ if (postSelectTimerRef.current) {
123
+ clearTimeout(postSelectTimerRef.current);
124
+ postSelectTimerRef.current = null;
125
+ }
126
+ }, []);
127
+
128
+ const clearEndUploadTimer = useCallback(() => {
129
+ if (endUploadTimerRef.current) {
130
+ clearTimeout(endUploadTimerRef.current);
131
+ endUploadTimerRef.current = null;
132
+ }
133
+ }, []);
134
+
135
+ useEffect(() => {
136
+ isActiveRef.current = true;
137
+ return () => {
138
+ isActiveRef.current = false;
139
+ clearPostSelectTimer();
140
+ clearEndUploadTimer();
141
+ };
142
+ }, [clearPostSelectTimer, clearEndUploadTimer]);
114
143
 
115
144
  const endUpload = useCallback(() => {
116
145
  const started = uploadStartRef.current;
117
146
  const elapsed = started ? Date.now() - started : MIN_BANNER_MS;
118
147
  const remaining = elapsed < MIN_BANNER_MS ? MIN_BANNER_MS - elapsed : 0;
119
- setTimeout(() => {
148
+ clearEndUploadTimer();
149
+ endUploadTimerRef.current = setTimeout(() => {
150
+ endUploadTimerRef.current = null;
151
+ if (!isActiveRef.current) return;
120
152
  setUploading(false);
121
153
  uploadStartRef.current = null;
122
154
  }, remaining);
123
- }, []);
155
+ }, [clearEndUploadTimer]);
124
156
 
125
157
  const processFileUploads = async (selectedFiles: UploadCandidate[]): Promise<FileMetadata[]> => {
126
158
  if (selectedFiles.length === 0) return [];
@@ -277,6 +309,11 @@ export const useFileUploadState = ({
277
309
 
278
310
  const fileType = candidateType(file);
279
311
 
312
+ if (imagesOnly && !fileType.startsWith('image/')) {
313
+ toast.error(t('fileManagement.toasts.imagesOnlyRequired'));
314
+ continue;
315
+ }
316
+
280
317
  // Generate preview for images - unified approach
281
318
  let preview: string | undefined;
282
319
  if (fileType.startsWith('image/')) {
@@ -311,7 +348,7 @@ export const useFileUploadState = ({
311
348
  // Show preview modal for user to review files before upload
312
349
  setPendingFiles(processedFiles);
313
350
  setShowUploadPreview(true);
314
- }, [t]);
351
+ }, [imagesOnly, t]);
315
352
 
316
353
  const handleConfirmUpload = async () => {
317
354
  if (pendingFiles.length === 0) return;
@@ -335,7 +372,11 @@ export const useFileUploadState = ({
335
372
  // If in selectMode, automatically select the uploaded file(s)
336
373
  if (selectMode && uploadedFiles.length > 0) {
337
374
  // Defer one tick so the query cache has settled before selecting.
338
- setTimeout(() => {
375
+ clearPostSelectTimer();
376
+ postSelectTimerRef.current = setTimeout(() => {
377
+ postSelectTimerRef.current = null;
378
+ if (!isActiveRef.current) return;
379
+
339
380
  const fileToSelect = uploadedFiles[0];
340
381
  if (!multiSelect && fileToSelect) {
341
382
  if (onPicked) {
@@ -371,6 +412,8 @@ export const useFileUploadState = ({
371
412
  };
372
413
 
373
414
  const handleCancelUpload = () => {
415
+ clearPostSelectTimer();
416
+
374
417
  // Cleanup preview URLs
375
418
  for (const pf of pendingFiles) {
376
419
  revokePreviewUrl(pf.preview);
@@ -412,7 +455,7 @@ export const useFileUploadState = ({
412
455
  // Use expo-document-picker (works on all platforms including web)
413
456
  // On web, it uses the native file input and provides File objects directly
414
457
  const result = await picker.getDocumentAsync({
415
- type: '*/*',
458
+ type: imagesOnly ? 'image/*' : '*/*',
416
459
  multiple: true,
417
460
  copyToCacheDirectory: true,
418
461
  });
@@ -9,6 +9,7 @@ import {
9
9
  OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY,
10
10
  OXY_SILENT_OAUTH_ATTEMPTED_KEY,
11
11
  persistOAuthHandshake,
12
+ consumeOAuthReturnPath,
12
13
  normalizeOAuthRedirectUri,
13
14
  logger,
14
15
  } from '@oxyhq/core';
@@ -157,7 +158,15 @@ export function consumeSilentOAuthError(): 'login_required' | 'consent_required'
157
158
  url.searchParams.delete('error');
158
159
  url.searchParams.delete('error_description');
159
160
  url.searchParams.delete('state');
160
- history.replaceState(history.state, '', `${url.pathname}${url.search}${url.hash}`);
161
+ // Silent restore failed, but the visitor still asked for a specific page.
162
+ // Put them back on it rather than leaving them on the bare origin the IdP
163
+ // redirected to.
164
+ const returnPath = consumeOAuthReturnPath();
165
+ history.replaceState(
166
+ history.state,
167
+ '',
168
+ returnPath ?? `${url.pathname}${url.search}${url.hash}`,
169
+ );
161
170
  }
162
171
  return error;
163
172
  }
@@ -1,6 +1,7 @@
1
1
  import type { OxyServices } from '@oxyhq/core';
2
2
  import {
3
3
  clearOAuthHandshake,
4
+ consumeOAuthReturnPath,
4
5
  logger,
5
6
  normalizeOAuthRedirectUri,
6
7
  readOAuthHandshake,
@@ -100,7 +101,15 @@ function stripOAuthParamsFromUrl(): void {
100
101
  url.searchParams.delete('error');
101
102
  url.searchParams.delete('error_description');
102
103
  url.searchParams.delete('hub_sync');
103
- history.replaceState(history.state, '', `${url.pathname}${url.search}${url.hash}`);
104
+ // The authorize round trip lands on the registered apex origin, so restore
105
+ // the page the visit started on. Applies to the success path too: signing in
106
+ // from a deep link should return you to that link, not to the home page.
107
+ const returnPath = consumeOAuthReturnPath();
108
+ history.replaceState(
109
+ history.state,
110
+ '',
111
+ returnPath ?? `${url.pathname}${url.search}${url.hash}`,
112
+ );
104
113
  }
105
114
 
106
115
  /**