@oxyhq/services 11.1.0 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/index.js +9 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/OxyLogo.js +1 -1
- package/lib/commonjs/ui/components/OxyLogo.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +0 -2
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +79 -10
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +24 -3
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/screens/ProfileScreen.js +2 -28
- package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/commonjs/ui/utils/storageHelpers.js +2 -1
- package/lib/commonjs/ui/utils/storageHelpers.js.map +1 -1
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/OxyLogo.js +1 -1
- package/lib/module/ui/components/OxyLogo.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +0 -2
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +80 -11
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +24 -3
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/index.js +2 -0
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/ProfileScreen.js +3 -29
- package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/module/ui/utils/storageHelpers.js +2 -1
- package/lib/module/ui/utils/storageHelpers.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +0 -10
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +9 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +3 -0
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +0 -17
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/storageHelpers.d.ts +11 -0
- package/lib/typescript/commonjs/ui/utils/storageHelpers.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +0 -10
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +9 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +3 -0
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ProfileScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +0 -17
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/storageHelpers.d.ts +11 -0
- package/lib/typescript/module/ui/utils/storageHelpers.d.ts.map +1 -1
- package/lib/typescript/types/react-native-classname.d.ts +39 -0
- package/package.json +3 -3
- package/src/index.ts +3 -0
- package/src/types/react-native-classname.d.ts +39 -0
- package/src/ui/components/OxyLogo.tsx +1 -1
- package/src/ui/components/OxyProvider.tsx +0 -2
- package/src/ui/context/OxyContext.tsx +81 -19
- package/src/ui/context/hooks/useAuthOperations.ts +35 -2
- package/src/ui/index.ts +2 -0
- package/src/ui/screens/ProfileScreen.tsx +5 -47
- package/src/ui/types/navigation.ts +0 -17
- package/src/ui/utils/storageHelpers.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/services",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.0",
|
|
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",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
108
|
"dependencies": {
|
|
109
|
-
"@oxyhq/contracts": "
|
|
109
|
+
"@oxyhq/contracts": "0.4.0"
|
|
110
110
|
},
|
|
111
111
|
"devDependencies": {
|
|
112
112
|
"nativewind": "5.0.0-preview.3",
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
"peerDependencies": {
|
|
153
153
|
"@expo/vector-icons": "^15.0.3",
|
|
154
154
|
"@oxyhq/bloom": ">=0.16.0",
|
|
155
|
-
"@oxyhq/core": "^3.
|
|
155
|
+
"@oxyhq/core": "^3.15.0",
|
|
156
156
|
"@react-native-async-storage/async-storage": "^2.0.0",
|
|
157
157
|
"@react-native-community/netinfo": "^11.4.1",
|
|
158
158
|
"@tanstack/query-async-storage-persister": "^5.100",
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
/// <reference path="./types/react-native-classname.d.ts" />
|
|
25
|
+
|
|
24
26
|
import { setPlatformOS, type PlatformOS } from '@oxyhq/core';
|
|
25
27
|
import { Platform } from 'react-native';
|
|
26
28
|
setPlatformOS(Platform.OS as PlatformOS);
|
|
@@ -166,6 +168,7 @@ export {
|
|
|
166
168
|
// Mutation status aggregator (for "Syncing..." indicators)
|
|
167
169
|
export { useMutationStatus } from './ui/hooks/useMutationStatus';
|
|
168
170
|
export type { MutationStatus } from './ui/hooks/useMutationStatus';
|
|
171
|
+
export { useOnlineStatus } from './ui/hooks/useOnlineStatus';
|
|
169
172
|
|
|
170
173
|
// ---------------------------------------------------------------------------
|
|
171
174
|
// Error handlers
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Robust NativeWind className typings for React Native's `ScrollView` & `FlatList`.
|
|
2
|
+
//
|
|
3
|
+
// NativeWind 5's engine (`react-native-css`) augments `ScrollViewProps` and
|
|
4
|
+
// `FlatListProps` with className props, but its declarations carry heritage
|
|
5
|
+
// clauses (`interface ScrollViewProps extends ViewProps, ScrollViewPropsIOS, …`)
|
|
6
|
+
// and route `FlatList`'s `contentContainerClassName` through a deep
|
|
7
|
+
// `@react-native/virtualized-lists` → `ScrollViewProps` augmentation. Under
|
|
8
|
+
// React Native 0.85 that heritage no longer matches, and the nested
|
|
9
|
+
// `@react-native/virtualized-lists` copy does not resolve from a CONSUMER's
|
|
10
|
+
// `node_modules` — so when this package's source is type-checked through the
|
|
11
|
+
// `react-native` export condition (which resolves `@oxyhq/services` to raw
|
|
12
|
+
// `src/`), those className members silently drop and consumer `tsc` fails with
|
|
13
|
+
// TS2769 on screens that use `contentContainerClassName` (e.g.
|
|
14
|
+
// ManageAccountScreen, ConnectedAppsScreen).
|
|
15
|
+
//
|
|
16
|
+
// These heritage-free augmentations add the className props directly to the two
|
|
17
|
+
// interfaces, so resolution never depends on the deep virtualized-lists copy or
|
|
18
|
+
// on react-native-css's heritage clauses. The plain `import 'react-native'`
|
|
19
|
+
// makes this a module so the `declare module` is treated as an augmentation
|
|
20
|
+
// (merge) rather than a redeclaration. Consumers load it via the
|
|
21
|
+
// `/// <reference path>` directives in `src/index.ts` and `src/ui/index.ts`.
|
|
22
|
+
|
|
23
|
+
import 'react-native';
|
|
24
|
+
|
|
25
|
+
declare module 'react-native' {
|
|
26
|
+
interface ScrollViewProps {
|
|
27
|
+
className?: string;
|
|
28
|
+
contentContainerClassName?: string;
|
|
29
|
+
indicatorClassName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FlatListProps<ItemT> {
|
|
33
|
+
className?: string;
|
|
34
|
+
contentContainerClassName?: string;
|
|
35
|
+
columnWrapperClassName?: string;
|
|
36
|
+
ListHeaderComponentClassName?: string;
|
|
37
|
+
ListFooterComponentClassName?: string;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -52,7 +52,7 @@ const IconLogo: React.FC<{ size: number; style?: ViewStyle; primary: string; inn
|
|
|
52
52
|
/>
|
|
53
53
|
<Path
|
|
54
54
|
d="M342.19 891.70425c-34.34375 2.53125-64.8125-3.85547-91.40625-19.15625-26.59375-15.3125-49.26172-36.85156-68-64.625-18.73047-27.78125-33.58594-60.5039-44.5625-98.17188-10.96875-37.67578-18.0039-77.625-21.10938-119.84375-2.89453-39.40625-1.46874-79.6953 4.28126-120.875 5.75-41.17578 16.42578-78.75 32.03124-112.71875 15.61329-33.97656 36.44532-62.39453 62.5-85.25 26.05079-22.85156 58.21875-35.6875 96.5-38.5 37.14454-2.72656 68.97657 4.98047 95.5 23.125 26.51954 18.13672 48.40625 42.5625 65.65625 73.28126 17.25782 30.71093 30.57032 65.53125 39.9375 104.46875 9.375 38.92968 15.50782 78.09375 18.40626 117.5 3.22656 43.90625 1.91406 85.74609-3.9375 125.51562-5.84375 39.76172-16.53907 75.21094-32.07813 106.34375-15.54297 31.13672-36.04297 56.41406-61.5 75.82813-25.44922 19.40625-56.1875 30.42968-92.21875 33.07812zm-24.5-471.89063c-10.69922.78125-19.53906 5.67188-26.51563 14.67188-6.98046 8.99219-12.49609 20.57031-16.54687 34.73438-4.04297 14.16796-6.5625 29.92187-7.5625 47.26562-.99219 17.33594-.84375 34.73047.4375 52.1875 1.44531 19.69922 4.16016 39.30469 8.14063 58.8125 3.98828 19.51172 9.09375 36.96484 15.3125 52.35938 6.22656 15.39843 13.49218 27.60156 21.79687 36.60937 8.30078 9 17.80078 13.10938 28.5 12.32813 12.38281-.91407 21.78125-7.83204 28.1875-20.75 6.40625-12.91407 11.07813-28.25391 14.01563-46.01563 2.9453-17.75781 4.4414-35.97656 4.48437-54.65625.03906-18.6875-.39844-34.21875-1.3125-46.59375-1.11719-15.19531-3.28125-31.17188-6.5-47.92188-3.21094-16.75-7.59375-32.1328-13.15625-46.15625-5.55469-14.01953-12.33984-25.54687-20.35938-34.57812-8.02343-9.03125-17.66406-13.1289-28.92187-12.29688z"
|
|
55
|
-
fill=
|
|
55
|
+
fill={inner}
|
|
56
56
|
/>
|
|
57
57
|
</G>
|
|
58
58
|
</G>
|
|
@@ -113,7 +113,6 @@ const OxyProvider: FC<OxyProviderProps> = ({
|
|
|
113
113
|
baseURL,
|
|
114
114
|
authWebUrl,
|
|
115
115
|
authRedirectUri,
|
|
116
|
-
disableAutoSso,
|
|
117
116
|
queryClient: providedQueryClient,
|
|
118
117
|
}) => {
|
|
119
118
|
|
|
@@ -309,7 +308,6 @@ const OxyProvider: FC<OxyProviderProps> = ({
|
|
|
309
308
|
authRedirectUri={authRedirectUri}
|
|
310
309
|
storageKeyPrefix={storageKeyPrefix}
|
|
311
310
|
clientId={clientId}
|
|
312
|
-
disableAutoSso={disableAutoSso}
|
|
313
311
|
onAuthStateChange={onAuthStateChange as OxyContextProviderProps['onAuthStateChange']}
|
|
314
312
|
>
|
|
315
313
|
{children}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
ssoAttemptedKey,
|
|
29
29
|
isCentralIdPOrigin,
|
|
30
30
|
guardActive,
|
|
31
|
+
allowSsoBounce,
|
|
31
32
|
ssoNavigate,
|
|
32
33
|
buildSsoBounceUrl,
|
|
33
34
|
consumeSsoReturn,
|
|
@@ -196,16 +197,6 @@ export interface OxyContextProviderProps {
|
|
|
196
197
|
* for the cross-app device sign-in flow. See {@link OxyContextState.clientId}.
|
|
197
198
|
*/
|
|
198
199
|
clientId?: string;
|
|
199
|
-
/**
|
|
200
|
-
* When `true`, skips ONLY the terminal `sso-bounce` cold-boot step — the
|
|
201
|
-
* force-redirect to `auth.<apex>/sso?prompt=none` that fires for a visitor
|
|
202
|
-
* with no recoverable local session. Every other cold-boot step still runs
|
|
203
|
-
* (callback consume, FedCM silent, `/auth/silent` iframe, stored-session,
|
|
204
|
-
* cookie-restore), so a returning signed-in user is still silently
|
|
205
|
-
* restored; only the bounce for a truly anonymous visitor is suppressed.
|
|
206
|
-
* Default `false`.
|
|
207
|
-
*/
|
|
208
|
-
disableAutoSso?: boolean;
|
|
209
200
|
onAuthStateChange?: (user: User | null) => void;
|
|
210
201
|
onError?: (error: ApiError) => void;
|
|
211
202
|
}
|
|
@@ -462,7 +453,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
462
453
|
authRedirectUri,
|
|
463
454
|
storageKeyPrefix = 'oxy_session',
|
|
464
455
|
clientId: clientIdProp,
|
|
465
|
-
disableAutoSso = false,
|
|
466
456
|
onAuthStateChange,
|
|
467
457
|
onError,
|
|
468
458
|
}) => {
|
|
@@ -631,6 +621,25 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
631
621
|
return storageReady.promise;
|
|
632
622
|
}, [storageReady]);
|
|
633
623
|
|
|
624
|
+
// Clear the durable "had a signed-in Oxy session before" hint.
|
|
625
|
+
//
|
|
626
|
+
// The hint (`storageKeys.priorSession`) is WRITTEN whenever a session is
|
|
627
|
+
// established/restored — every commit funnels through `persistSessionDurably`,
|
|
628
|
+
// and the stored-session reload winner sets it directly to backfill
|
|
629
|
+
// pre-existing installs. It lives in the SAME `storageKeyPrefix`-scoped
|
|
630
|
+
// durable store as the session ids, so it SURVIVES a session expiring; it is
|
|
631
|
+
// cleared ONLY here, on EXPLICIT full sign-out (wired into `clearAllAccountData`
|
|
632
|
+
// and the `useAuthOperations` logout paths — never the passive token-expiry
|
|
633
|
+
// path). At cold boot the hint is read into `hadPriorSession` and feeds
|
|
634
|
+
// `allowSsoBounce`: a RETURNING visitor (hint present) whose local session has
|
|
635
|
+
// lapsed still gets ONE terminal `/sso` establish bounce so a central-only
|
|
636
|
+
// cross-domain session recovers, while a truly first-time anonymous visitor is
|
|
637
|
+
// never force-redirected.
|
|
638
|
+
const clearPriorSessionHint = useCallback(async (): Promise<void> => {
|
|
639
|
+
const readyStorage = await getReadyStorage();
|
|
640
|
+
await readyStorage.removeItem(storageKeys.priorSession);
|
|
641
|
+
}, [getReadyStorage, storageKeys.priorSession]);
|
|
642
|
+
|
|
634
643
|
useEffect(() => {
|
|
635
644
|
let mounted = true;
|
|
636
645
|
createPlatformStorage()
|
|
@@ -717,6 +726,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
717
726
|
updateSessions,
|
|
718
727
|
saveActiveSessionId,
|
|
719
728
|
clearSessionState,
|
|
729
|
+
clearPriorSessionHint,
|
|
720
730
|
switchSession,
|
|
721
731
|
applyLanguagePreference,
|
|
722
732
|
onAuthStateChange,
|
|
@@ -745,12 +755,18 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
745
755
|
// Clear session state (sessions, activeSessionId, storage)
|
|
746
756
|
await clearSessionState();
|
|
747
757
|
|
|
758
|
+
// Explicit FULL sign-out: drop the durable returning-user hint so the next
|
|
759
|
+
// cold boot treats this device as a first-time anonymous visitor (no forced
|
|
760
|
+
// `/sso` bounce). NOT done on the passive token-expiry path, so an expired
|
|
761
|
+
// session still recovers via a returning-user bounce.
|
|
762
|
+
await clearPriorSessionHint();
|
|
763
|
+
|
|
748
764
|
// Reset account store
|
|
749
765
|
useAccountStore.getState().reset();
|
|
750
766
|
|
|
751
767
|
// Clear HTTP service cache
|
|
752
768
|
oxyServices.clearCache();
|
|
753
|
-
}, [queryClient, storage, clearSessionState, logger, oxyServices]);
|
|
769
|
+
}, [queryClient, storage, clearSessionState, clearPriorSessionHint, logger, oxyServices]);
|
|
754
770
|
|
|
755
771
|
const { getDeviceSessions, logoutAllDeviceSessions, updateDeviceName } = useDeviceManagement({
|
|
756
772
|
oxyServices,
|
|
@@ -831,7 +847,13 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
831
847
|
sessionIds.push(sessionId);
|
|
832
848
|
await readyStorage.setItem(storageKeys.sessionIds, JSON.stringify(sessionIds));
|
|
833
849
|
}
|
|
834
|
-
|
|
850
|
+
// A session is now durably committed — set the returning-user hint so a
|
|
851
|
+
// future cold boot whose local session has lapsed still gets ONE `/sso`
|
|
852
|
+
// establish bounce (see `markPriorSessionHint`). Every web commit path
|
|
853
|
+
// (FedCM / silent iframe / SSO return / password / cookie restore) funnels
|
|
854
|
+
// through here, so this is the single chokepoint for the hint.
|
|
855
|
+
await readyStorage.setItem(storageKeys.priorSession, '1');
|
|
856
|
+
}, [getReadyStorage, logger, storageKeys.activeSessionId, storageKeys.sessionIds, storageKeys.priorSession]);
|
|
835
857
|
|
|
836
858
|
// Refs so the cold-boot restore can plant session state without widening its
|
|
837
859
|
// dependency array (mirrors the existing ref pattern above).
|
|
@@ -1073,6 +1095,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1073
1095
|
// immediately so the loading screen clears without waiting for the
|
|
1074
1096
|
// remaining cold-boot steps to be evaluated/short-circuited (idempotent).
|
|
1075
1097
|
markAuthResolvedRef.current();
|
|
1098
|
+
// Backfill the returning-user hint. A reload winner already had the hint
|
|
1099
|
+
// set at original sign-in, but pre-existing installs (signed in before
|
|
1100
|
+
// this hint shipped) get it set here so their NEXT lapse-and-return still
|
|
1101
|
+
// earns one `/sso` establish bounce. `storage` is non-null (guarded at
|
|
1102
|
+
// the top of this callback); best-effort, never blocks restore.
|
|
1103
|
+
await storage.setItem(storageKeys.priorSession, '1');
|
|
1076
1104
|
return true;
|
|
1077
1105
|
} catch (switchError) {
|
|
1078
1106
|
// Silently handle expected errors (invalid sessions, timeouts, network issues)
|
|
@@ -1102,6 +1130,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1102
1130
|
storage,
|
|
1103
1131
|
storageKeys.activeSessionId,
|
|
1104
1132
|
storageKeys.sessionIds,
|
|
1133
|
+
storageKeys.priorSession,
|
|
1105
1134
|
]);
|
|
1106
1135
|
|
|
1107
1136
|
// Shared in-flight `runSsoReturn` promise — see the CONCURRENCY note on
|
|
@@ -1213,6 +1242,27 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1213
1242
|
// `stored-session` skips, this stays false, and the probes run as before.
|
|
1214
1243
|
let storedSessionRestored = false;
|
|
1215
1244
|
|
|
1245
|
+
// FIX-B smart-gate input: has this device/app EVER had a signed-in Oxy
|
|
1246
|
+
// session (the durable `priorSession` hint, set on every commit, cleared
|
|
1247
|
+
// only on explicit full sign-out)? Read ONCE here — synchronously usable by
|
|
1248
|
+
// the terminal `sso-bounce` `enabled` gate below — so a RETURNING visitor
|
|
1249
|
+
// whose local session has lapsed still earns one `/sso` establish bounce,
|
|
1250
|
+
// while a truly first-time anonymous visitor is never force-redirected.
|
|
1251
|
+
// `storage` is non-null (guarded above); a read failure is treated as "no
|
|
1252
|
+
// prior session" (fail safe toward anonymous-browse).
|
|
1253
|
+
let hadPriorSession = false;
|
|
1254
|
+
try {
|
|
1255
|
+
hadPriorSession = (await storage.getItem(storageKeys.priorSession)) === '1';
|
|
1256
|
+
} catch (priorSessionReadError) {
|
|
1257
|
+
if (__DEV__) {
|
|
1258
|
+
loggerUtil.debug(
|
|
1259
|
+
'Failed to read prior-session hint (treating as first-time visitor)',
|
|
1260
|
+
{ component: 'OxyContext', method: 'restoreSessionsFromStorage' },
|
|
1261
|
+
priorSessionReadError as unknown,
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1216
1266
|
try {
|
|
1217
1267
|
const outcome = await runColdBoot<true>({
|
|
1218
1268
|
steps: [
|
|
@@ -1400,11 +1450,23 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1400
1450
|
// still active (loop + self-heal protection).
|
|
1401
1451
|
id: 'sso-bounce',
|
|
1402
1452
|
enabled: () => {
|
|
1403
|
-
//
|
|
1404
|
-
//
|
|
1405
|
-
//
|
|
1406
|
-
//
|
|
1407
|
-
|
|
1453
|
+
// Smart gate (SDK-owned, shared with `WebOxyProvider` via core's
|
|
1454
|
+
// `allowSsoBounce`). The terminal `/sso` establish-bounce is the
|
|
1455
|
+
// ONLY cold-boot step that can recover a session living SOLELY at
|
|
1456
|
+
// the central IdP (a cross-apex RP whose local session expired),
|
|
1457
|
+
// and it is what plants the per-apex `fedcm_session` cookie the
|
|
1458
|
+
// earlier silent-iframe step relies on. So it is allowed iff a
|
|
1459
|
+
// prior-signed-in hint exists OR a local session was recovered this
|
|
1460
|
+
// boot (`storedSessionRestored`, always false here — an earlier step
|
|
1461
|
+
// would have won — but passed for spec fidelity): a RETURNING user
|
|
1462
|
+
// still gets ONE bounce, while a truly first-time anonymous visitor
|
|
1463
|
+
// does NOT (anonymous browse). The per-tab loop guards below
|
|
1464
|
+
// (`ssoNoSessionKey`, `ssoAttemptedKey`, `guardActive`) still cap an
|
|
1465
|
+
// allowed bounce at one per cold boot.
|
|
1466
|
+
if (!allowSsoBounce({
|
|
1467
|
+
hasPriorSession: hadPriorSession,
|
|
1468
|
+
hasLocalSession: storedSessionRestored,
|
|
1469
|
+
})) {
|
|
1408
1470
|
return false;
|
|
1409
1471
|
}
|
|
1410
1472
|
if (!isWebBrowser() || window.top !== window.self) {
|
|
@@ -1497,11 +1559,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1497
1559
|
}, [
|
|
1498
1560
|
oxyServices,
|
|
1499
1561
|
storage,
|
|
1562
|
+
storageKeys.priorSession,
|
|
1500
1563
|
restoreViaRefreshCookie,
|
|
1501
1564
|
restoreStoredSession,
|
|
1502
1565
|
runSsoReturn,
|
|
1503
1566
|
markAuthResolved,
|
|
1504
|
-
disableAutoSso,
|
|
1505
1567
|
]);
|
|
1506
1568
|
|
|
1507
1569
|
useEffect(() => {
|
|
@@ -20,6 +20,14 @@ export interface UseAuthOperationsOptions {
|
|
|
20
20
|
updateSessions: (sessions: ClientSession[], options?: { merge?: boolean }) => void;
|
|
21
21
|
saveActiveSessionId: (sessionId: string) => Promise<void>;
|
|
22
22
|
clearSessionState: () => Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Clear the durable returning-user hint (`storageKeys.priorSession`). Called
|
|
25
|
+
* ONLY on EXPLICIT full sign-out — alongside `clearSsoBounceState()` — so the
|
|
26
|
+
* next cold boot treats this device as a first-time anonymous visitor (no
|
|
27
|
+
* forced `/sso` bounce). NEVER called on the passive token-expiry path, so an
|
|
28
|
+
* expired session still recovers via a returning-user bounce. Best-effort.
|
|
29
|
+
*/
|
|
30
|
+
clearPriorSessionHint: () => Promise<void>;
|
|
23
31
|
switchSession: (sessionId: string) => Promise<User>;
|
|
24
32
|
applyLanguagePreference: (user: User) => Promise<void>;
|
|
25
33
|
onAuthStateChange?: (user: User | null) => void;
|
|
@@ -44,6 +52,24 @@ const LOGIN_ERROR_CODE = 'LOGIN_ERROR';
|
|
|
44
52
|
const LOGOUT_ERROR_CODE = 'LOGOUT_ERROR';
|
|
45
53
|
const LOGOUT_ALL_ERROR_CODE = 'LOGOUT_ALL_ERROR';
|
|
46
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Fire-and-forget the durable returning-user hint clear on explicit sign-out.
|
|
57
|
+
*
|
|
58
|
+
* Mirrors the synchronous, non-blocking nature of the sibling
|
|
59
|
+
* `clearSsoBounceState()`: sign-out must NEVER block on (or fail because of) a
|
|
60
|
+
* best-effort storage write. The clear is invoked synchronously (so unit tests
|
|
61
|
+
* can assert it ran) but its async settle is detached; any rejection is logged,
|
|
62
|
+
* never thrown.
|
|
63
|
+
*/
|
|
64
|
+
function clearPriorSessionHintSafe(
|
|
65
|
+
clearPriorSessionHint: () => Promise<void>,
|
|
66
|
+
logger?: (message: string, error?: unknown) => void,
|
|
67
|
+
): void {
|
|
68
|
+
clearPriorSessionHint().catch((hintError) => {
|
|
69
|
+
logger?.('Failed to clear prior-session hint on sign-out', hintError);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
47
73
|
/**
|
|
48
74
|
* Authentication operations using public key cryptography.
|
|
49
75
|
* Accepts public key as parameter - identity management is handled by the app layer.
|
|
@@ -56,6 +82,7 @@ export const useAuthOperations = ({
|
|
|
56
82
|
updateSessions,
|
|
57
83
|
saveActiveSessionId,
|
|
58
84
|
clearSessionState,
|
|
85
|
+
clearPriorSessionHint,
|
|
59
86
|
switchSession,
|
|
60
87
|
applyLanguagePreference,
|
|
61
88
|
onAuthStateChange,
|
|
@@ -227,6 +254,7 @@ export const useAuthOperations = ({
|
|
|
227
254
|
// Genuine FULL sign-out (no sessions remain): clear the per-origin
|
|
228
255
|
// SSO bounce state so a fresh deliberate sign-in can re-probe.
|
|
229
256
|
clearSsoBounceState();
|
|
257
|
+
clearPriorSessionHintSafe(clearPriorSessionHint, logger);
|
|
230
258
|
await clearSessionState();
|
|
231
259
|
return;
|
|
232
260
|
}
|
|
@@ -237,6 +265,7 @@ export const useAuthOperations = ({
|
|
|
237
265
|
if (isInvalid && targetSessionId === activeSessionId) {
|
|
238
266
|
// The active session is invalid → full sign-out; clear SSO state too.
|
|
239
267
|
clearSsoBounceState();
|
|
268
|
+
clearPriorSessionHintSafe(clearPriorSessionHint, logger);
|
|
240
269
|
await clearSessionState();
|
|
241
270
|
return;
|
|
242
271
|
}
|
|
@@ -254,6 +283,7 @@ export const useAuthOperations = ({
|
|
|
254
283
|
[
|
|
255
284
|
activeSessionId,
|
|
256
285
|
clearSessionState,
|
|
286
|
+
clearPriorSessionHint,
|
|
257
287
|
logger,
|
|
258
288
|
onError,
|
|
259
289
|
oxyServices,
|
|
@@ -286,8 +316,11 @@ export const useAuthOperations = ({
|
|
|
286
316
|
clearActiveAuthuser();
|
|
287
317
|
}
|
|
288
318
|
// logoutAll is ALWAYS a full sign-out: clear the per-origin SSO bounce
|
|
289
|
-
// state (web-guarded internally) so a fresh sign-in can re-probe
|
|
319
|
+
// state (web-guarded internally) so a fresh sign-in can re-probe, and drop
|
|
320
|
+
// the durable returning-user hint so the next cold boot is treated as a
|
|
321
|
+
// first-time anonymous visitor (no forced `/sso` bounce after sign-out).
|
|
290
322
|
clearSsoBounceState();
|
|
323
|
+
clearPriorSessionHintSafe(clearPriorSessionHint, logger);
|
|
291
324
|
await clearSessionState();
|
|
292
325
|
} catch (error) {
|
|
293
326
|
handleAuthError(error, {
|
|
@@ -299,7 +332,7 @@ export const useAuthOperations = ({
|
|
|
299
332
|
});
|
|
300
333
|
throw error instanceof Error ? error : new Error('Logout all failed');
|
|
301
334
|
}
|
|
302
|
-
}, [activeSessionId, clearSessionState, logger, onError, oxyServices, setAuthState]);
|
|
335
|
+
}, [activeSessionId, clearSessionState, clearPriorSessionHint, logger, onError, oxyServices, setAuthState]);
|
|
303
336
|
|
|
304
337
|
return {
|
|
305
338
|
signIn,
|
package/src/ui/index.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* cannot contain `require()` per CLAUDE.md) and (b) defeated tree-shaking.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
/// <reference path="../types/react-native-classname.d.ts" />
|
|
16
|
+
|
|
15
17
|
// Components
|
|
16
18
|
export { default as OxyProvider } from './components/OxyProvider';
|
|
17
19
|
export { default as OxySignInButton } from './components/OxySignInButton';
|
|
@@ -12,8 +12,8 @@ import { useFollow } from '../hooks/useFollow';
|
|
|
12
12
|
import { Ionicons } from '@expo/vector-icons';
|
|
13
13
|
import { useI18n } from '../hooks/useI18n';
|
|
14
14
|
import { useOxy } from '../context/OxyContext';
|
|
15
|
-
import { getAccountDisplayName, logger } from '@oxyhq/core';
|
|
16
|
-
import type { User } from '@oxyhq/core';
|
|
15
|
+
import { getAccountDisplayName, logger, normalizeProfileLinks } from '@oxyhq/core';
|
|
16
|
+
import type { User, ProfileLink } from '@oxyhq/core';
|
|
17
17
|
import { extractErrorMessage } from '../utils/errorHandlers';
|
|
18
18
|
|
|
19
19
|
interface ProfileScreenProps extends BaseScreenProps {
|
|
@@ -21,16 +21,6 @@ interface ProfileScreenProps extends BaseScreenProps {
|
|
|
21
21
|
username?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
interface LinkMetadata {
|
|
25
|
-
id?: string;
|
|
26
|
-
url: string;
|
|
27
|
-
title?: string;
|
|
28
|
-
description?: string;
|
|
29
|
-
image?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type ProfileLink = string | { link: string } | LinkMetadata;
|
|
33
|
-
|
|
34
24
|
const AVATAR_SIZE = 96;
|
|
35
25
|
const BANNER_HEIGHT = 160;
|
|
36
26
|
const AVATAR_OVERLAP = -56;
|
|
@@ -45,13 +35,7 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme,
|
|
|
45
35
|
const [commentsCount, setCommentsCount] = useState<number | null>(null);
|
|
46
36
|
const [isLoading, setIsLoading] = useState(true);
|
|
47
37
|
const [error, setError] = useState<string | null>(null);
|
|
48
|
-
const [links, setLinks] = useState<
|
|
49
|
-
url: string;
|
|
50
|
-
title?: string;
|
|
51
|
-
description?: string;
|
|
52
|
-
image?: string;
|
|
53
|
-
id: string;
|
|
54
|
-
}>>([]);
|
|
38
|
+
const [links, setLinks] = useState<ProfileLink[]>([]);
|
|
55
39
|
|
|
56
40
|
// Use the follow hook for real follower data
|
|
57
41
|
const {
|
|
@@ -114,34 +98,8 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme,
|
|
|
114
98
|
setProfile(profileRes);
|
|
115
99
|
setReputationTotal(typeof reputationRes.total === 'number' ? reputationRes.total : null);
|
|
116
100
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
const linksWithIds = profileRes.linksMetadata.map((link: LinkMetadata, index: number) => ({
|
|
120
|
-
...link,
|
|
121
|
-
id: link.id || `existing-${index}`
|
|
122
|
-
}));
|
|
123
|
-
setLinks(linksWithIds);
|
|
124
|
-
} else if (Array.isArray(profileRes.links)) {
|
|
125
|
-
const simpleLinks = profileRes.links.map((l: ProfileLink) => typeof l === 'string' ? l : (typeof l === 'object' && 'link' in l ? l.link : '')).filter(Boolean);
|
|
126
|
-
const linksWithMetadata = simpleLinks.map((url: string, index: number) => ({
|
|
127
|
-
url,
|
|
128
|
-
title: url.replace(/^https?:\/\//, '').replace(/\/$/, ''),
|
|
129
|
-
description: `Link to ${url}`,
|
|
130
|
-
image: undefined,
|
|
131
|
-
id: `existing-${index}`
|
|
132
|
-
}));
|
|
133
|
-
setLinks(linksWithMetadata);
|
|
134
|
-
} else if (profileRes.website) {
|
|
135
|
-
setLinks([{
|
|
136
|
-
url: profileRes.website,
|
|
137
|
-
title: profileRes.website.replace(/^https?:\/\//, '').replace(/\/$/, ''),
|
|
138
|
-
description: `Link to ${profileRes.website}`,
|
|
139
|
-
image: undefined,
|
|
140
|
-
id: 'existing-0'
|
|
141
|
-
}]);
|
|
142
|
-
} else {
|
|
143
|
-
setLinks([]);
|
|
144
|
-
}
|
|
101
|
+
// Normalize profile links via the shared @oxyhq/core helper.
|
|
102
|
+
setLinks(normalizeProfileLinks(profileRes.linksMetadata, profileRes.links));
|
|
145
103
|
|
|
146
104
|
// Follower/following counts are managed by the `useFollow` hook.
|
|
147
105
|
|
|
@@ -63,21 +63,4 @@ export interface OxyProviderProps {
|
|
|
63
63
|
authWebUrl?: string;
|
|
64
64
|
authRedirectUri?: string;
|
|
65
65
|
queryClient?: QueryClient;
|
|
66
|
-
/**
|
|
67
|
-
* When `true`, skips ONLY the terminal SSO bounce in the web cold-boot
|
|
68
|
-
* chain — the force-redirect to `auth.<apex>/sso?prompt=none` that fires
|
|
69
|
-
* for a visitor with no recoverable local session. This lets a truly
|
|
70
|
-
* anonymous user keep browsing instead of being bounced to the central
|
|
71
|
-
* IdP (e.g. a marketplace that allows anonymous browsing like eBay /
|
|
72
|
-
* Shop.app).
|
|
73
|
-
*
|
|
74
|
-
* Session restore still runs in full: the callback consume, FedCM silent,
|
|
75
|
-
* first-party `/auth/silent` iframe, stored-session bearer, and
|
|
76
|
-
* cookie-restore steps all execute — so a returning signed-in user is
|
|
77
|
-
* still silently restored. Only the force-bounce for a genuinely
|
|
78
|
-
* anonymous visitor is suppressed.
|
|
79
|
-
*
|
|
80
|
-
* Default `false` (current behavior: the bounce fires).
|
|
81
|
-
*/
|
|
82
|
-
disableAutoSso?: boolean;
|
|
83
66
|
}
|
|
@@ -9,6 +9,17 @@ export interface SessionStorageKeys {
|
|
|
9
9
|
activeSessionId: string;
|
|
10
10
|
sessionIds: string;
|
|
11
11
|
language: string;
|
|
12
|
+
/**
|
|
13
|
+
* DURABLE "this device/app has had a signed-in Oxy session before" hint.
|
|
14
|
+
*
|
|
15
|
+
* Lives in the SAME `storageKeyPrefix`-scoped durable store as the stored
|
|
16
|
+
* session ids, so it survives a session expiring (cleared only on explicit
|
|
17
|
+
* full sign-out). Read at cold boot to drive the smart `sso-bounce` gate:
|
|
18
|
+
* a returning visitor still gets ONE establish bounce so a central-only
|
|
19
|
+
* cross-domain session recovers, while a first-time anonymous visitor is
|
|
20
|
+
* never force-redirected. See `allowSsoBounce` in `@oxyhq/core`.
|
|
21
|
+
*/
|
|
22
|
+
priorSession: string;
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
/**
|
|
@@ -125,6 +136,7 @@ export const getStorageKeys = (prefix: string = STORAGE_KEY_PREFIX): SessionStor
|
|
|
125
136
|
activeSessionId: `${prefix}_active_session_id`,
|
|
126
137
|
sessionIds: `${prefix}_session_ids`,
|
|
127
138
|
language: `${prefix}_language`,
|
|
139
|
+
priorSession: `${prefix}_prior_session`,
|
|
128
140
|
});
|
|
129
141
|
|
|
130
142
|
|