@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.
- package/lib/commonjs/ui/components/oauthNavigation.js +11 -1
- package/lib/commonjs/ui/components/oauthNavigation.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSurfaceHeader.js +44 -7
- package/lib/commonjs/ui/hooks/useSurfaceHeader.js.map +1 -1
- package/lib/commonjs/ui/navigation/surfaces.js +8 -3
- package/lib/commonjs/ui/navigation/surfaces.js.map +1 -1
- package/lib/commonjs/ui/screens/CreateAccountScreen.js +11 -1
- package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +1 -0
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +10 -7
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/UploadBar.js +19 -11
- package/lib/commonjs/ui/screens/fileManagement/UploadBar.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +41 -5
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/commonjs/ui/utils/crossOriginRestore.js +5 -1
- package/lib/commonjs/ui/utils/crossOriginRestore.js.map +1 -1
- package/lib/commonjs/ui/utils/oauthReturn.js +5 -1
- package/lib/commonjs/ui/utils/oauthReturn.js.map +1 -1
- package/lib/module/ui/components/oauthNavigation.js +12 -2
- package/lib/module/ui/components/oauthNavigation.js.map +1 -1
- package/lib/module/ui/hooks/useSurfaceHeader.js +45 -8
- package/lib/module/ui/hooks/useSurfaceHeader.js.map +1 -1
- package/lib/module/ui/navigation/surfaces.js +7 -1
- package/lib/module/ui/navigation/surfaces.js.map +1 -1
- package/lib/module/ui/screens/CreateAccountScreen.js +12 -2
- package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +1 -0
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +10 -7
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/UploadBar.js +20 -12
- package/lib/module/ui/screens/fileManagement/UploadBar.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +42 -6
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/module/ui/utils/crossOriginRestore.js +6 -2
- package/lib/module/ui/utils/crossOriginRestore.js.map +1 -1
- package/lib/module/ui/utils/oauthReturn.js +6 -2
- package/lib/module/ui/utils/oauthReturn.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/oauthNavigation.d.ts +7 -0
- package/lib/typescript/commonjs/ui/components/oauthNavigation.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts +7 -0
- package/lib/typescript/commonjs/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/surfaces.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/UploadBar.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +3 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/crossOriginRestore.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/oauthReturn.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/oauthNavigation.d.ts +7 -0
- package/lib/typescript/module/ui/components/oauthNavigation.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts +7 -0
- package/lib/typescript/module/ui/hooks/useSurfaceHeader.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/surfaces.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/UploadBar.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +3 -1
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/crossOriginRestore.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/oauthReturn.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/ui/components/oauthNavigation.ts +12 -2
- package/src/ui/hooks/useSurfaceHeader.ts +65 -8
- package/src/ui/navigation/surfaces.ts +9 -1
- package/src/ui/screens/CreateAccountScreen.tsx +13 -2
- package/src/ui/screens/FileManagementScreen.tsx +1 -0
- package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +4 -5
- package/src/ui/screens/fileManagement/UploadBar.tsx +17 -10
- package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +49 -6
- package/src/ui/utils/crossOriginRestore.ts +10 -1
- 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,
|
|
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;
|
|
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;
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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'}>
|
|
@@ -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 (
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|