@oxyhq/auth 2.0.4 → 2.0.6
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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/WebOxyProvider.js +37 -0
- package/dist/cjs/hooks/mutations/index.js +5 -1
- package/dist/cjs/hooks/mutations/useAccountMutations.js +185 -43
- package/dist/cjs/hooks/mutations/useAppData.js +133 -0
- package/dist/cjs/hooks/queries/appDataQueryKeys.js +46 -0
- package/dist/cjs/hooks/queries/index.js +8 -1
- package/dist/cjs/hooks/queries/useAppData.js +87 -0
- package/dist/cjs/hooks/queryClient.js +136 -92
- package/dist/cjs/hooks/useFileDownloadUrl.js +12 -36
- package/dist/cjs/hooks/useSessionSocket.js +81 -94
- package/dist/cjs/index.js +8 -3
- package/dist/cjs/utils/sessionHelpers.js +3 -1
- package/dist/cjs/utils/storageHelpers.js +36 -10
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/WebOxyProvider.js +38 -1
- package/dist/esm/hooks/mutations/index.js +2 -0
- package/dist/esm/hooks/mutations/useAccountMutations.js +186 -44
- package/dist/esm/hooks/mutations/useAppData.js +128 -0
- package/dist/esm/hooks/queries/appDataQueryKeys.js +42 -0
- package/dist/esm/hooks/queries/index.js +3 -0
- package/dist/esm/hooks/queries/useAppData.js +82 -0
- package/dist/esm/hooks/queryClient.js +132 -89
- package/dist/esm/hooks/useFileDownloadUrl.js +11 -34
- package/dist/esm/hooks/useSessionSocket.js +81 -94
- package/dist/esm/index.js +3 -3
- package/dist/esm/utils/sessionHelpers.js +3 -1
- package/dist/esm/utils/storageHelpers.js +36 -10
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/WebOxyProvider.d.ts +1 -1
- package/dist/types/hooks/mutations/index.d.ts +1 -0
- package/dist/types/hooks/mutations/useAccountMutations.d.ts +153 -9
- package/dist/types/hooks/mutations/useAppData.d.ts +47 -0
- package/dist/types/hooks/queries/appDataQueryKeys.d.ts +24 -0
- package/dist/types/hooks/queries/index.d.ts +2 -0
- package/dist/types/hooks/queries/useAccountQueries.d.ts +11 -7
- package/dist/types/hooks/queries/useAppData.d.ts +46 -0
- package/dist/types/hooks/queries/useSecurityQueries.d.ts +2 -2
- package/dist/types/hooks/queries/useServicesQueries.d.ts +7 -5
- package/dist/types/hooks/queryClient.d.ts +24 -10
- package/dist/types/hooks/useAssets.d.ts +1 -1
- package/dist/types/hooks/useFileDownloadUrl.d.ts +2 -6
- package/dist/types/index.d.ts +3 -3
- package/dist/types/utils/sessionHelpers.d.ts +3 -1
- package/package.json +22 -3
- package/src/WebOxyProvider.tsx +39 -1
- package/src/hooks/mutations/index.ts +3 -0
- package/src/hooks/mutations/useAccountMutations.ts +230 -57
- package/src/hooks/mutations/useAppData.ts +167 -0
- package/src/hooks/queries/appDataQueryKeys.ts +53 -0
- package/src/hooks/queries/index.ts +4 -0
- package/src/hooks/queries/useAppData.ts +105 -0
- package/src/hooks/queryClient.ts +140 -83
- package/src/hooks/useFileDownloadUrl.ts +15 -39
- package/src/hooks/useSessionSocket.ts +123 -91
- package/src/index.ts +7 -1
- package/src/utils/sessionHelpers.ts +3 -1
- package/src/utils/storageHelpers.ts +49 -10
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-Data Query Hooks
|
|
3
|
+
*
|
|
4
|
+
* Read side of the `/users/me/app-data/...` per-user JSON KV store. Gated on
|
|
5
|
+
* `isAuthenticated` — when signed out the query stays `enabled: false` and
|
|
6
|
+
* `data` is `null`, so consumers can fall back to localStorage without ever
|
|
7
|
+
* issuing a doomed request.
|
|
8
|
+
*
|
|
9
|
+
* Errors from the network (404 because the endpoint isn't deployed yet,
|
|
10
|
+
* 401 because the session lapsed, etc.) are not user-facing here. Hooks
|
|
11
|
+
* return `data: null` on error so the calling component renders the
|
|
12
|
+
* "nothing yet" state and the consuming app can quietly fall back to local
|
|
13
|
+
* persistence. Mutations still propagate errors so write attempts surface
|
|
14
|
+
* a toast — only reads are silent.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
|
|
18
|
+
import { authenticatedApiCall } from '@oxyhq/core';
|
|
19
|
+
import { useWebOxy } from '../../WebOxyProvider';
|
|
20
|
+
import { appDataQueryKeys, isMissingAppDataEndpointError } from './appDataQueryKeys';
|
|
21
|
+
|
|
22
|
+
interface AppDataQueryOptions {
|
|
23
|
+
/** Disable the query without unmounting the component. */
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
/** Override the default 1-minute stale time. */
|
|
26
|
+
staleTime?: number;
|
|
27
|
+
/** Override the default 30-minute gc time. */
|
|
28
|
+
gcTime?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read a single per-user JSON value.
|
|
33
|
+
*
|
|
34
|
+
* @param namespace - kebab/snake-case identifier (e.g. `"academy"`).
|
|
35
|
+
* @param key - kebab/snake-case identifier (e.g. course slug).
|
|
36
|
+
* @param options - optional `enabled`/`staleTime`/`gcTime` overrides.
|
|
37
|
+
*
|
|
38
|
+
* @returns A `useQuery` result with `data` of type `T | null`. The query
|
|
39
|
+
* stays disabled when the user is signed out; when enabled but the server
|
|
40
|
+
* has no stored value, `data` is `null`. Reads that fail because the
|
|
41
|
+
* endpoint isn't reachable also resolve to `null` so the consumer can
|
|
42
|
+
* fall back to local persistence.
|
|
43
|
+
*/
|
|
44
|
+
export const useAppData = <T = unknown>(
|
|
45
|
+
namespace: string,
|
|
46
|
+
key: string,
|
|
47
|
+
options?: AppDataQueryOptions,
|
|
48
|
+
): UseQueryResult<T | null, Error> => {
|
|
49
|
+
const { oxyServices, activeSessionId, isAuthenticated } = useWebOxy();
|
|
50
|
+
|
|
51
|
+
return useQuery<T | null, Error>({
|
|
52
|
+
queryKey: appDataQueryKeys.value(namespace, key),
|
|
53
|
+
queryFn: async () => {
|
|
54
|
+
try {
|
|
55
|
+
return await authenticatedApiCall(oxyServices, activeSessionId, () =>
|
|
56
|
+
oxyServices.getAppData<T>(namespace, key),
|
|
57
|
+
);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Endpoint not deployed yet, no network, etc. — return null so the
|
|
60
|
+
// consumer falls back to localStorage rather than rendering a broken
|
|
61
|
+
// UI state. Authentication errors still bubble up so the auth retry
|
|
62
|
+
// pipeline can surface them at the provider level.
|
|
63
|
+
if (isMissingAppDataEndpointError(error)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
enabled: (options?.enabled !== false) && isAuthenticated,
|
|
70
|
+
staleTime: options?.staleTime ?? 60 * 1000,
|
|
71
|
+
gcTime: options?.gcTime ?? 30 * 60 * 1000,
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read every value in a namespace.
|
|
77
|
+
*
|
|
78
|
+
* @returns A `useQuery` result with `data` as a `Record<string, T>`. Empty
|
|
79
|
+
* object when the namespace contains nothing (or when fetching failed).
|
|
80
|
+
*/
|
|
81
|
+
export const useAppDataNamespace = <T = unknown>(
|
|
82
|
+
namespace: string,
|
|
83
|
+
options?: AppDataQueryOptions,
|
|
84
|
+
): UseQueryResult<Record<string, T>, Error> => {
|
|
85
|
+
const { oxyServices, activeSessionId, isAuthenticated } = useWebOxy();
|
|
86
|
+
|
|
87
|
+
return useQuery<Record<string, T>, Error>({
|
|
88
|
+
queryKey: appDataQueryKeys.namespace(namespace),
|
|
89
|
+
queryFn: async () => {
|
|
90
|
+
try {
|
|
91
|
+
return await authenticatedApiCall(oxyServices, activeSessionId, () =>
|
|
92
|
+
oxyServices.listAppData<T>(namespace),
|
|
93
|
+
);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (isMissingAppDataEndpointError(error)) {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
enabled: (options?.enabled !== false) && isAuthenticated,
|
|
102
|
+
staleTime: options?.staleTime ?? 60 * 1000,
|
|
103
|
+
gcTime: options?.gcTime ?? 30 * 60 * 1000,
|
|
104
|
+
});
|
|
105
|
+
};
|
package/src/hooks/queryClient.ts
CHANGED
|
@@ -1,112 +1,169 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Web QueryClient with offline-first defaults + localStorage persistence.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the persistence behaviour in `@oxyhq/services/queryClient` so
|
|
5
|
+
* web auth apps (FedCM, popup, redirect flows) survive a page reload with
|
|
6
|
+
* cached identity + paused mutations intact.
|
|
7
|
+
*
|
|
8
|
+
* Persistence is opt-in via `attachQueryPersistence(...)` so SSR callers
|
|
9
|
+
* (Next.js getServerSideProps, Vite SSR, tests) can create a stateless
|
|
10
|
+
* client without touching `window`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { QueryClient, type Mutation, type Query } from '@tanstack/react-query';
|
|
14
|
+
import {
|
|
15
|
+
persistQueryClient,
|
|
16
|
+
type PersistedClient,
|
|
17
|
+
} from '@tanstack/react-query-persist-client';
|
|
18
|
+
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
|
|
2
19
|
import type { StorageInterface } from '../utils/storageHelpers';
|
|
3
20
|
|
|
4
|
-
const QUERY_CACHE_KEY = '
|
|
5
|
-
const
|
|
21
|
+
const QUERY_CACHE_KEY = 'oxy_auth_query_cache_v2';
|
|
22
|
+
const QUERY_CACHE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
|
|
23
|
+
const QUERY_PERSIST_THROTTLE_MS = 1_000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Query-key prefixes whose data is safe to restore across reloads.
|
|
27
|
+
* Web auth surfaces are session/profile heavy — lists and history are not
|
|
28
|
+
* persisted to keep the localStorage footprint small.
|
|
29
|
+
*/
|
|
30
|
+
const PERSISTED_QUERY_PREFIXES: ReadonlyArray<string> = [
|
|
31
|
+
'accounts',
|
|
32
|
+
'users',
|
|
33
|
+
'sessions',
|
|
34
|
+
'auth',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function shouldDehydrateQuery(query: Query): boolean {
|
|
38
|
+
if (query.state.status !== 'success') return false;
|
|
39
|
+
const head = query.queryKey[0];
|
|
40
|
+
return typeof head === 'string' && PERSISTED_QUERY_PREFIXES.includes(head);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shouldDehydrateMutation(_mutation: Mutation): boolean {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
6
46
|
|
|
7
47
|
/**
|
|
8
|
-
*
|
|
48
|
+
* Best-effort detection — works in browsers, Node SSR, and React Server
|
|
49
|
+
* Components. `localStorage` is gated behind `window` because Node and edge
|
|
50
|
+
* runtimes may polyfill `globalThis.localStorage` inconsistently.
|
|
9
51
|
*/
|
|
10
|
-
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
52
|
+
function getBrowserLocalStorage(): Storage | null {
|
|
53
|
+
if (typeof window === 'undefined') return null;
|
|
54
|
+
try {
|
|
55
|
+
if (!window.localStorage) return null;
|
|
56
|
+
return window.localStorage;
|
|
57
|
+
} catch {
|
|
58
|
+
// Access blocked (Safari Private Mode, sandboxed iframe, etc.)
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const createPersistenceAdapter = (storage: StorageInterface) => ({
|
|
64
|
+
persistClient: async (client: unknown): Promise<void> => {
|
|
65
|
+
try {
|
|
66
|
+
await storage.setItem(QUERY_CACHE_KEY, JSON.stringify(client));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
69
|
+
console.warn('[QueryClient] Failed to persist cache', error);
|
|
24
70
|
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (parsed.version !== QUERY_CACHE_VERSION) {
|
|
35
|
-
// Clear old cache on version mismatch
|
|
36
|
-
await storage.removeItem(QUERY_CACHE_KEY);
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check if cache is too old (30 days)
|
|
41
|
-
const maxAge = 30 * 24 * 60 * 60 * 1000;
|
|
42
|
-
if (parsed.timestamp && Date.now() - parsed.timestamp > maxAge) {
|
|
43
|
-
await storage.removeItem(QUERY_CACHE_KEY);
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return parsed.clientState;
|
|
48
|
-
} catch (error) {
|
|
49
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
50
|
-
console.warn('[QueryClient] Failed to restore cache:', error);
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
restoreClient: async (): Promise<unknown> => {
|
|
74
|
+
try {
|
|
75
|
+
const cached = await storage.getItem(QUERY_CACHE_KEY);
|
|
76
|
+
return cached ? JSON.parse(cached) : undefined;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
79
|
+
console.warn('[QueryClient] Failed to restore cache', error);
|
|
53
80
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
removeClient: async (): Promise<void> => {
|
|
85
|
+
try {
|
|
86
|
+
await storage.removeItem(QUERY_CACHE_KEY);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
89
|
+
console.warn('[QueryClient] Failed to remove cache', error);
|
|
62
90
|
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
};
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
});
|
|
66
94
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*/
|
|
70
|
-
export const createQueryClient = (storage?: StorageInterface | null): QueryClient => {
|
|
71
|
-
const client = new QueryClient({
|
|
95
|
+
export const createQueryClient = (): QueryClient =>
|
|
96
|
+
new QueryClient({
|
|
72
97
|
defaultOptions: {
|
|
73
98
|
queries: {
|
|
74
|
-
// Data is fresh for 5 minutes
|
|
75
99
|
staleTime: 5 * 60 * 1000,
|
|
76
|
-
|
|
77
|
-
gcTime: 10 * 60 * 1000,
|
|
78
|
-
// Retry 3 times with exponential backoff
|
|
100
|
+
gcTime: 30 * 60 * 1000,
|
|
79
101
|
retry: 3,
|
|
80
|
-
retryDelay: (
|
|
81
|
-
// Refetch on reconnect
|
|
102
|
+
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
|
|
82
103
|
refetchOnReconnect: true,
|
|
83
|
-
// Don't refetch on window focus (better for mobile)
|
|
84
104
|
refetchOnWindowFocus: false,
|
|
85
|
-
// Offline-first: use cache when offline
|
|
86
105
|
networkMode: 'offlineFirst',
|
|
87
106
|
},
|
|
88
107
|
mutations: {
|
|
89
|
-
// Retry once for mutations
|
|
90
108
|
retry: 1,
|
|
91
|
-
// Offline-first: queue mutations when offline
|
|
92
109
|
networkMode: 'offlineFirst',
|
|
93
110
|
},
|
|
94
111
|
},
|
|
95
112
|
});
|
|
96
113
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return client;
|
|
103
|
-
};
|
|
114
|
+
export interface AttachPersistenceResult {
|
|
115
|
+
restored: Promise<void>;
|
|
116
|
+
unsubscribe: () => void;
|
|
117
|
+
}
|
|
104
118
|
|
|
105
119
|
/**
|
|
106
|
-
*
|
|
120
|
+
* Wire `persistQueryClient` to browser `localStorage` (or a no-op when not
|
|
121
|
+
* in a browser). Returns the restore promise so consumers can `await` it
|
|
122
|
+
* before exposing the client to <Suspense> boundaries.
|
|
107
123
|
*/
|
|
108
|
-
export const
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
export const attachQueryPersistence = (
|
|
125
|
+
queryClient: QueryClient,
|
|
126
|
+
): AttachPersistenceResult => {
|
|
127
|
+
const localStorage = getBrowserLocalStorage();
|
|
128
|
+
if (!localStorage) {
|
|
129
|
+
return { restored: Promise.resolve(), unsubscribe: () => {} };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const persister = createSyncStoragePersister({
|
|
133
|
+
storage: localStorage,
|
|
134
|
+
key: QUERY_CACHE_KEY,
|
|
135
|
+
throttleTime: QUERY_PERSIST_THROTTLE_MS,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const [unsubscribe, restored] = persistQueryClient({
|
|
139
|
+
queryClient,
|
|
140
|
+
persister,
|
|
141
|
+
maxAge: QUERY_CACHE_MAX_AGE,
|
|
142
|
+
dehydrateOptions: {
|
|
143
|
+
shouldDehydrateQuery,
|
|
144
|
+
shouldDehydrateMutation,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
restored.catch((error) => {
|
|
149
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
150
|
+
console.warn('[QueryClient] Failed to restore persisted cache', error);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return { unsubscribe, restored };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const clearQueryCache = async (
|
|
158
|
+
storage: StorageInterface,
|
|
159
|
+
): Promise<void> => {
|
|
160
|
+
try {
|
|
161
|
+
await storage.removeItem(QUERY_CACHE_KEY);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
164
|
+
console.warn('[QueryClient] Failed to remove cache', error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
111
167
|
};
|
|
112
168
|
|
|
169
|
+
export type { PersistedClient };
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import { OxyServices } from '@oxyhq/core';
|
|
3
|
-
|
|
4
|
-
let oxyInstance: OxyServices | null = null;
|
|
5
|
-
|
|
6
|
-
export const setOxyFileUrlInstance = (instance: OxyServices) => {
|
|
7
|
-
oxyInstance = instance;
|
|
8
|
-
};
|
|
2
|
+
import type { OxyServices } from '@oxyhq/core';
|
|
9
3
|
|
|
10
4
|
export interface UseFileDownloadUrlOptions {
|
|
11
5
|
variant?: string;
|
|
@@ -21,38 +15,21 @@ export interface UseFileDownloadUrlResult {
|
|
|
21
15
|
/**
|
|
22
16
|
* Hook to resolve a file's download URL asynchronously.
|
|
23
17
|
*
|
|
24
|
-
* Prefers the provided `oxyServices` instance, falls back to the module-level
|
|
25
|
-
* singleton set via `setOxyFileUrlInstance`.
|
|
26
|
-
*
|
|
27
18
|
* Uses `getFileDownloadUrlAsync` first, falling back to the synchronous
|
|
28
19
|
* `getFileDownloadUrl` if the async call fails.
|
|
29
20
|
*/
|
|
30
21
|
export const useFileDownloadUrl = (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
oxyServices: OxyServices | null | undefined,
|
|
23
|
+
fileId: string | null | undefined,
|
|
24
|
+
options?: UseFileDownloadUrlOptions,
|
|
34
25
|
): UseFileDownloadUrlResult => {
|
|
35
|
-
// Support two call signatures:
|
|
36
|
-
// 1. useFileDownloadUrl(oxyServices, fileId, options) — preferred
|
|
37
|
-
// 2. useFileDownloadUrl(fileId, options) — legacy (uses singleton)
|
|
38
|
-
let services: OxyServices | null;
|
|
39
|
-
let fileId: string | null | undefined;
|
|
40
|
-
let options: UseFileDownloadUrlOptions | undefined;
|
|
41
|
-
|
|
42
|
-
if (fileIdOrServices instanceof OxyServices) {
|
|
43
|
-
services = fileIdOrServices;
|
|
44
|
-
fileId = typeof fileIdOrOptions === 'string' ? fileIdOrOptions : null;
|
|
45
|
-
options = maybeOptions;
|
|
46
|
-
} else {
|
|
47
|
-
services = oxyInstance;
|
|
48
|
-
fileId = typeof fileIdOrServices === 'string' ? fileIdOrServices : null;
|
|
49
|
-
options = typeof fileIdOrOptions === 'object' && fileIdOrOptions !== null ? fileIdOrOptions as UseFileDownloadUrlOptions : undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
26
|
const [url, setUrl] = useState<string | null>(null);
|
|
53
27
|
const [loading, setLoading] = useState(false);
|
|
54
28
|
const [error, setError] = useState<Error | null>(null);
|
|
55
29
|
|
|
30
|
+
const variant = options?.variant;
|
|
31
|
+
const expiresIn = options?.expiresIn;
|
|
32
|
+
|
|
56
33
|
useEffect(() => {
|
|
57
34
|
if (!fileId) {
|
|
58
35
|
setUrl(null);
|
|
@@ -61,7 +38,7 @@ export const useFileDownloadUrl = (
|
|
|
61
38
|
return;
|
|
62
39
|
}
|
|
63
40
|
|
|
64
|
-
if (!
|
|
41
|
+
if (!oxyServices) {
|
|
65
42
|
setUrl(null);
|
|
66
43
|
setLoading(false);
|
|
67
44
|
setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
|
|
@@ -69,22 +46,22 @@ export const useFileDownloadUrl = (
|
|
|
69
46
|
}
|
|
70
47
|
|
|
71
48
|
let cancelled = false;
|
|
72
|
-
const instance =
|
|
49
|
+
const instance = oxyServices;
|
|
50
|
+
const targetFileId = fileId;
|
|
73
51
|
|
|
74
52
|
const load = async () => {
|
|
75
53
|
setLoading(true);
|
|
76
54
|
setError(null);
|
|
77
55
|
|
|
78
56
|
try {
|
|
79
|
-
const { variant, expiresIn } = options || {};
|
|
80
57
|
let resolvedUrl: string | null = null;
|
|
81
58
|
|
|
82
59
|
if (typeof instance.getFileDownloadUrlAsync === 'function') {
|
|
83
|
-
resolvedUrl = await instance.getFileDownloadUrlAsync(
|
|
60
|
+
resolvedUrl = await instance.getFileDownloadUrlAsync(targetFileId, variant, expiresIn);
|
|
84
61
|
}
|
|
85
62
|
|
|
86
63
|
if (!resolvedUrl && typeof instance.getFileDownloadUrl === 'function') {
|
|
87
|
-
resolvedUrl = instance.getFileDownloadUrl(
|
|
64
|
+
resolvedUrl = instance.getFileDownloadUrl(targetFileId, variant, expiresIn);
|
|
88
65
|
}
|
|
89
66
|
|
|
90
67
|
if (!cancelled) {
|
|
@@ -94,8 +71,7 @@ export const useFileDownloadUrl = (
|
|
|
94
71
|
// Fallback to sync URL on error where possible
|
|
95
72
|
try {
|
|
96
73
|
if (typeof instance.getFileDownloadUrl === 'function') {
|
|
97
|
-
const
|
|
98
|
-
const fallbackUrl = instance.getFileDownloadUrl(fileId!, variant, expiresIn);
|
|
74
|
+
const fallbackUrl = instance.getFileDownloadUrl(targetFileId, variant, expiresIn);
|
|
99
75
|
if (!cancelled) {
|
|
100
76
|
setUrl(fallbackUrl || null);
|
|
101
77
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -103,7 +79,7 @@ export const useFileDownloadUrl = (
|
|
|
103
79
|
return;
|
|
104
80
|
}
|
|
105
81
|
} catch {
|
|
106
|
-
//
|
|
82
|
+
// Secondary failure: surface the original error below.
|
|
107
83
|
}
|
|
108
84
|
|
|
109
85
|
if (!cancelled) {
|
|
@@ -121,7 +97,7 @@ export const useFileDownloadUrl = (
|
|
|
121
97
|
return () => {
|
|
122
98
|
cancelled = true;
|
|
123
99
|
};
|
|
124
|
-
}, [fileId,
|
|
100
|
+
}, [fileId, oxyServices, variant, expiresIn]);
|
|
125
101
|
|
|
126
102
|
return { url, loading, error };
|
|
127
103
|
};
|