@oxyhq/services 10.3.3 → 10.5.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/ui/components/AnotherDeviceQR.js +121 -0
- package/lib/commonjs/ui/components/AnotherDeviceQR.js.map +1 -0
- package/lib/commonjs/ui/components/OxyProvider.js +2 -0
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/SignInModal.js +145 -356
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +49 -9
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js +5 -0
- package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/commonjs/ui/hooks/useFollow.js +28 -12
- package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js +458 -0
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -0
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +62 -460
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/components/AnotherDeviceQR.js +116 -0
- package/lib/module/ui/components/AnotherDeviceQR.js.map +1 -0
- package/lib/module/ui/components/OxyProvider.js +2 -0
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/SignInModal.js +149 -358
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +49 -9
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/queries/queryKeys.js +5 -0
- package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/module/ui/hooks/useFollow.js +29 -13
- package/lib/module/ui/hooks/useFollow.js.map +1 -1
- package/lib/module/ui/hooks/useOxyAuthSession.js +452 -0
- package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -0
- package/lib/module/ui/screens/OxyAuthScreen.js +62 -460
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts +24 -0
- package/lib/typescript/commonjs/ui/components/AnotherDeviceQR.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts +10 -5
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +10 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts +4 -0
- package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +83 -0
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts +12 -8
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +17 -0
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts +24 -0
- package/lib/typescript/module/ui/components/AnotherDeviceQR.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SignInModal.d.ts +10 -5
- package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +10 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts +4 -0
- package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +83 -0
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts +12 -8
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +17 -0
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ui/components/AnotherDeviceQR.tsx +111 -0
- package/src/ui/components/OxyProvider.tsx +2 -0
- package/src/ui/components/SignInModal.tsx +142 -381
- package/src/ui/context/OxyContext.tsx +59 -8
- package/src/ui/hooks/queries/queryKeys.ts +6 -0
- package/src/ui/hooks/useFollow.ts +30 -12
- package/src/ui/hooks/useOxyAuthSession.ts +556 -0
- package/src/ui/screens/OxyAuthScreen.tsx +61 -506
- package/src/ui/types/navigation.ts +17 -0
|
@@ -161,6 +161,16 @@ export interface OxyContextProviderProps {
|
|
|
161
161
|
* for the cross-app device sign-in flow. See {@link OxyContextState.clientId}.
|
|
162
162
|
*/
|
|
163
163
|
clientId?: string;
|
|
164
|
+
/**
|
|
165
|
+
* When `true`, skips ONLY the terminal `sso-bounce` cold-boot step — the
|
|
166
|
+
* force-redirect to `auth.<apex>/sso?prompt=none` that fires for a visitor
|
|
167
|
+
* with no recoverable local session. Every other cold-boot step still runs
|
|
168
|
+
* (callback consume, FedCM silent, `/auth/silent` iframe, stored-session,
|
|
169
|
+
* cookie-restore), so a returning signed-in user is still silently
|
|
170
|
+
* restored; only the bounce for a truly anonymous visitor is suppressed.
|
|
171
|
+
* Default `false`.
|
|
172
|
+
*/
|
|
173
|
+
disableAutoSso?: boolean;
|
|
164
174
|
onAuthStateChange?: (user: User | null) => void;
|
|
165
175
|
onError?: (error: ApiError) => void;
|
|
166
176
|
}
|
|
@@ -254,6 +264,39 @@ const COOKIE_RESTORE_TIMEOUT = 3000;
|
|
|
254
264
|
*/
|
|
255
265
|
const COLD_BOOT_OVERALL_DEADLINE = 20000;
|
|
256
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Per-session timeout (ms) for the parallel stored-session validation in
|
|
269
|
+
* `restoreStoredSession`. Each `validateSession` call races against this timer
|
|
270
|
+
* so a single slow/offline session never blocks the whole startup validation
|
|
271
|
+
* sweep — sessions that don't answer in time resolve to `null` (treated as
|
|
272
|
+
* unvalidated) and the remaining sessions still settle.
|
|
273
|
+
*/
|
|
274
|
+
const VALIDATION_TIMEOUT = 8000;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Fallback client-session validity window (ms) — 7 days — applied when a
|
|
278
|
+
* restored account/session does not carry an explicit `expiresAt`. This is only
|
|
279
|
+
* a local display/bookkeeping hint for the multi-session store; the server
|
|
280
|
+
* remains the source of truth for actual session expiry. Used in the refresh
|
|
281
|
+
* cookie restore, stored-session restore, and web-SSO session paths.
|
|
282
|
+
*/
|
|
283
|
+
const DEFAULT_SESSION_VALIDITY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Minimum interval (ms) between visibility-driven IdP `/auth/session-check`
|
|
287
|
+
* probes. Debounces the hidden-iframe session check so rapid tab focus/blur
|
|
288
|
+
* cycles can't spawn a check-iframe storm; at most one probe runs per window.
|
|
289
|
+
*/
|
|
290
|
+
const IDP_SESSION_CHECK_COOLDOWN = 30000;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Hard timeout (ms) for a single visibility-driven IdP `/auth/session-check`
|
|
294
|
+
* iframe. If the IdP never posts a `oxy-session-check` message back, the iframe
|
|
295
|
+
* and its listener are torn down after this budget so a non-responsive check
|
|
296
|
+
* can never leak an iframe or a `message` listener.
|
|
297
|
+
*/
|
|
298
|
+
const IDP_SESSION_CHECK_TIMEOUT = 5000;
|
|
299
|
+
|
|
257
300
|
function getHttpStatus(error: unknown): number | undefined {
|
|
258
301
|
if (!error || typeof error !== 'object') {
|
|
259
302
|
return undefined;
|
|
@@ -367,6 +410,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
367
410
|
authRedirectUri,
|
|
368
411
|
storageKeyPrefix = 'oxy_session',
|
|
369
412
|
clientId: clientIdProp,
|
|
413
|
+
disableAutoSso = false,
|
|
370
414
|
onAuthStateChange,
|
|
371
415
|
onError,
|
|
372
416
|
}) => {
|
|
@@ -849,7 +893,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
849
893
|
const clientSessions: ClientSession[] = snapshot.accounts.map((account) => ({
|
|
850
894
|
sessionId: account.sessionId,
|
|
851
895
|
deviceId: '',
|
|
852
|
-
expiresAt: account.expiresAt || new Date(now.getTime() +
|
|
896
|
+
expiresAt: account.expiresAt || new Date(now.getTime() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
|
|
853
897
|
lastActive: now.toISOString(),
|
|
854
898
|
userId: account.user?.id,
|
|
855
899
|
isCurrent: account.sessionId === activeAccount.sessionId,
|
|
@@ -906,9 +950,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
906
950
|
let validSessions: ClientSession[] = [];
|
|
907
951
|
|
|
908
952
|
if (storedSessionIds.length > 0) {
|
|
909
|
-
// Validate all sessions in parallel (with
|
|
953
|
+
// Validate all sessions in parallel (with a per-session timeout) to avoid
|
|
910
954
|
// sequential blocking that freezes the app on startup
|
|
911
|
-
const VALIDATION_TIMEOUT = 8000;
|
|
912
955
|
const results = await Promise.allSettled(
|
|
913
956
|
storedSessionIds.map(async (sessionId) => {
|
|
914
957
|
const timeoutPromise = new Promise<null>((resolve) =>
|
|
@@ -931,7 +974,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
931
974
|
const clientSession: ClientSession = {
|
|
932
975
|
sessionId,
|
|
933
976
|
deviceId: '',
|
|
934
|
-
expiresAt: new Date(now.getTime() +
|
|
977
|
+
expiresAt: new Date(now.getTime() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
|
|
935
978
|
lastActive: now.toISOString(),
|
|
936
979
|
userId: validation.user.id?.toString() ?? '',
|
|
937
980
|
isCurrent: sessionId === storedActiveSessionId,
|
|
@@ -1241,6 +1284,13 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1241
1284
|
// still active (loop + self-heal protection).
|
|
1242
1285
|
id: 'sso-bounce',
|
|
1243
1286
|
enabled: () => {
|
|
1287
|
+
// Opt-out: when the consumer disabled auto-SSO, never bounce a
|
|
1288
|
+
// truly-anonymous visitor to the central IdP. All other restore
|
|
1289
|
+
// steps already ran above, so a signed-in user is still
|
|
1290
|
+
// recovered; only this terminal force-bounce is suppressed.
|
|
1291
|
+
if (disableAutoSso) {
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1244
1294
|
if (!isWebBrowser() || window.top !== window.self) {
|
|
1245
1295
|
return false;
|
|
1246
1296
|
}
|
|
@@ -1335,6 +1385,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1335
1385
|
restoreStoredSession,
|
|
1336
1386
|
runSsoReturn,
|
|
1337
1387
|
markAuthResolved,
|
|
1388
|
+
disableAutoSso,
|
|
1338
1389
|
]);
|
|
1339
1390
|
|
|
1340
1391
|
useEffect(() => {
|
|
@@ -1456,7 +1507,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1456
1507
|
const clientSession = {
|
|
1457
1508
|
sessionId: session.sessionId,
|
|
1458
1509
|
deviceId: session.deviceId || '',
|
|
1459
|
-
expiresAt: session.expiresAt || new Date(Date.now() +
|
|
1510
|
+
expiresAt: session.expiresAt || new Date(Date.now() + DEFAULT_SESSION_VALIDITY_MS).toISOString(),
|
|
1460
1511
|
lastActive: new Date().toISOString(),
|
|
1461
1512
|
userId: session.user.id?.toString() ?? '',
|
|
1462
1513
|
isCurrent: true,
|
|
@@ -1550,9 +1601,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1550
1601
|
const idpOrigin = resolvedAuthWebUrl || 'https://auth.oxy.so';
|
|
1551
1602
|
|
|
1552
1603
|
const checkIdPSession = () => {
|
|
1553
|
-
// Debounce: check at most once per
|
|
1604
|
+
// Debounce: check at most once per cooldown window
|
|
1554
1605
|
const now = Date.now();
|
|
1555
|
-
if (now - lastIdPCheckRef.current <
|
|
1606
|
+
if (now - lastIdPCheckRef.current < IDP_SESSION_CHECK_COOLDOWN) return;
|
|
1556
1607
|
lastIdPCheckRef.current = now;
|
|
1557
1608
|
|
|
1558
1609
|
// Clean up any in-flight check before starting a new one
|
|
@@ -1591,7 +1642,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1591
1642
|
|
|
1592
1643
|
window.addEventListener('message', handleMessage);
|
|
1593
1644
|
document.body.appendChild(iframe);
|
|
1594
|
-
setTimeout(cleanup,
|
|
1645
|
+
setTimeout(cleanup, IDP_SESSION_CHECK_TIMEOUT);
|
|
1595
1646
|
pendingIdPCleanupRef.current = cleanup;
|
|
1596
1647
|
};
|
|
1597
1648
|
|
|
@@ -80,6 +80,12 @@ export const queryKeys = {
|
|
|
80
80
|
list: () => [...queryKeys.connectedApps.all, 'list'] as const,
|
|
81
81
|
},
|
|
82
82
|
|
|
83
|
+
// Follow / social graph queries
|
|
84
|
+
follow: {
|
|
85
|
+
all: ['follow'] as const,
|
|
86
|
+
counts: (userId: string) => [...queryKeys.follow.all, 'counts', userId] as const,
|
|
87
|
+
},
|
|
88
|
+
|
|
83
89
|
// Payment / wallet / subscription queries
|
|
84
90
|
payments: {
|
|
85
91
|
all: ['payments'] as const,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { useCallback, useMemo
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
3
|
import { useFollowStore } from '../stores/followStore';
|
|
3
4
|
import { useOxy } from '../context/OxyContext';
|
|
4
|
-
import {
|
|
5
|
+
import { type OxyServices, type BulkFollowResult, type BulkUnfollowResult } from '@oxyhq/core';
|
|
5
6
|
import { useShallow } from 'zustand/react/shallow';
|
|
7
|
+
import { queryKeys } from './queries/queryKeys';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* useFollow — Hook for follow state management.
|
|
@@ -21,6 +23,9 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
21
23
|
const { oxyServices, canUsePrivateApi } = useOxy();
|
|
22
24
|
const userIds = useMemo(() => (Array.isArray(userId) ? userId : userId ? [userId] : []), [userId]);
|
|
23
25
|
const isSingleUser = typeof userId === 'string';
|
|
26
|
+
// Narrowed single-user id for use in closures (callbacks/queryFn) where TS
|
|
27
|
+
// can't carry the `isSingleUser` boolean back to a `string` narrowing.
|
|
28
|
+
const singleUserId: string | undefined = typeof userId === 'string' ? userId : undefined;
|
|
24
29
|
|
|
25
30
|
// Granular Zustand selectors — only re-render when THIS user's data changes
|
|
26
31
|
const isFollowing = useFollowStore(
|
|
@@ -128,16 +133,29 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
128
133
|
useFollowStore.getState().setFollowingCount(userId, count);
|
|
129
134
|
}, [isSingleUser, userId]);
|
|
130
135
|
|
|
131
|
-
// Auto-fetch counts
|
|
132
|
-
useEffect
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
// Auto-fetch counts for single-user mode via React Query instead of a manual
|
|
137
|
+
// useEffect. The Zustand store remains the canonical home for count values
|
|
138
|
+
// (it is also written by toggleFollowUser / updateCountsFromFollowAction), so
|
|
139
|
+
// components keep reading `followerCount` / `followingCount` through the
|
|
140
|
+
// granular selectors above — this query only owns the FETCH lifecycle
|
|
141
|
+
// (dedup, caching, retry/backoff). It stays disabled until the counts are
|
|
142
|
+
// actually missing, reproducing the old effect's `(followerCount === null ||
|
|
143
|
+
// followingCount === null)` gate without a render-phase side effect. The
|
|
144
|
+
// store action does the network call and writes the counts; the query simply
|
|
145
|
+
// surfaces the resolved pair as its cached data.
|
|
146
|
+
useQuery({
|
|
147
|
+
queryKey: singleUserId ? queryKeys.follow.counts(singleUserId) : queryKeys.follow.all,
|
|
148
|
+
queryFn: async () => {
|
|
149
|
+
if (!singleUserId) return null;
|
|
150
|
+
await useFollowStore.getState().fetchUserCounts(singleUserId, oxyServices);
|
|
151
|
+
const state = useFollowStore.getState();
|
|
152
|
+
return {
|
|
153
|
+
followers: state.followerCounts[singleUserId] ?? null,
|
|
154
|
+
following: state.followingCounts[singleUserId] ?? null,
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
enabled: !!singleUserId && followerCount === null && followingCount === null,
|
|
158
|
+
});
|
|
141
159
|
|
|
142
160
|
// Multi-user callbacks
|
|
143
161
|
const toggleFollowForUser = useCallback(async (targetUserId: string) => {
|