@oxyhq/auth 2.0.4 → 2.0.5
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/useAccountMutations.js +185 -43
- 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 +1 -2
- 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/useAccountMutations.js +186 -44
- 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 +1 -1
- 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/useAccountMutations.d.ts +153 -9
- package/dist/types/hooks/queries/useAccountQueries.d.ts +11 -7
- 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 +1 -1
- package/dist/types/utils/sessionHelpers.d.ts +3 -1
- package/package.json +22 -3
- package/src/WebOxyProvider.tsx +39 -1
- package/src/hooks/mutations/useAccountMutations.ts +230 -57
- 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 +1 -1
- package/src/utils/sessionHelpers.ts +3 -1
- package/src/utils/storageHelpers.ts +49 -10
|
@@ -1,38 +1,16 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import { OxyServices } from '@oxyhq/core';
|
|
3
|
-
let oxyInstance = null;
|
|
4
|
-
export const setOxyFileUrlInstance = (instance) => {
|
|
5
|
-
oxyInstance = instance;
|
|
6
|
-
};
|
|
7
2
|
/**
|
|
8
3
|
* Hook to resolve a file's download URL asynchronously.
|
|
9
4
|
*
|
|
10
|
-
* Prefers the provided `oxyServices` instance, falls back to the module-level
|
|
11
|
-
* singleton set via `setOxyFileUrlInstance`.
|
|
12
|
-
*
|
|
13
5
|
* Uses `getFileDownloadUrlAsync` first, falling back to the synchronous
|
|
14
6
|
* `getFileDownloadUrl` if the async call fails.
|
|
15
7
|
*/
|
|
16
|
-
export const useFileDownloadUrl = (
|
|
17
|
-
// Support two call signatures:
|
|
18
|
-
// 1. useFileDownloadUrl(oxyServices, fileId, options) — preferred
|
|
19
|
-
// 2. useFileDownloadUrl(fileId, options) — legacy (uses singleton)
|
|
20
|
-
let services;
|
|
21
|
-
let fileId;
|
|
22
|
-
let options;
|
|
23
|
-
if (fileIdOrServices instanceof OxyServices) {
|
|
24
|
-
services = fileIdOrServices;
|
|
25
|
-
fileId = typeof fileIdOrOptions === 'string' ? fileIdOrOptions : null;
|
|
26
|
-
options = maybeOptions;
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
services = oxyInstance;
|
|
30
|
-
fileId = typeof fileIdOrServices === 'string' ? fileIdOrServices : null;
|
|
31
|
-
options = typeof fileIdOrOptions === 'object' && fileIdOrOptions !== null ? fileIdOrOptions : undefined;
|
|
32
|
-
}
|
|
8
|
+
export const useFileDownloadUrl = (oxyServices, fileId, options) => {
|
|
33
9
|
const [url, setUrl] = useState(null);
|
|
34
10
|
const [loading, setLoading] = useState(false);
|
|
35
11
|
const [error, setError] = useState(null);
|
|
12
|
+
const variant = options?.variant;
|
|
13
|
+
const expiresIn = options?.expiresIn;
|
|
36
14
|
useEffect(() => {
|
|
37
15
|
if (!fileId) {
|
|
38
16
|
setUrl(null);
|
|
@@ -40,25 +18,25 @@ export const useFileDownloadUrl = (fileIdOrServices, fileIdOrOptions, maybeOptio
|
|
|
40
18
|
setError(null);
|
|
41
19
|
return;
|
|
42
20
|
}
|
|
43
|
-
if (!
|
|
21
|
+
if (!oxyServices) {
|
|
44
22
|
setUrl(null);
|
|
45
23
|
setLoading(false);
|
|
46
24
|
setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
|
|
47
25
|
return;
|
|
48
26
|
}
|
|
49
27
|
let cancelled = false;
|
|
50
|
-
const instance =
|
|
28
|
+
const instance = oxyServices;
|
|
29
|
+
const targetFileId = fileId;
|
|
51
30
|
const load = async () => {
|
|
52
31
|
setLoading(true);
|
|
53
32
|
setError(null);
|
|
54
33
|
try {
|
|
55
|
-
const { variant, expiresIn } = options || {};
|
|
56
34
|
let resolvedUrl = null;
|
|
57
35
|
if (typeof instance.getFileDownloadUrlAsync === 'function') {
|
|
58
|
-
resolvedUrl = await instance.getFileDownloadUrlAsync(
|
|
36
|
+
resolvedUrl = await instance.getFileDownloadUrlAsync(targetFileId, variant, expiresIn);
|
|
59
37
|
}
|
|
60
38
|
if (!resolvedUrl && typeof instance.getFileDownloadUrl === 'function') {
|
|
61
|
-
resolvedUrl = instance.getFileDownloadUrl(
|
|
39
|
+
resolvedUrl = instance.getFileDownloadUrl(targetFileId, variant, expiresIn);
|
|
62
40
|
}
|
|
63
41
|
if (!cancelled) {
|
|
64
42
|
setUrl(resolvedUrl || null);
|
|
@@ -68,8 +46,7 @@ export const useFileDownloadUrl = (fileIdOrServices, fileIdOrOptions, maybeOptio
|
|
|
68
46
|
// Fallback to sync URL on error where possible
|
|
69
47
|
try {
|
|
70
48
|
if (typeof instance.getFileDownloadUrl === 'function') {
|
|
71
|
-
const
|
|
72
|
-
const fallbackUrl = instance.getFileDownloadUrl(fileId, variant, expiresIn);
|
|
49
|
+
const fallbackUrl = instance.getFileDownloadUrl(targetFileId, variant, expiresIn);
|
|
73
50
|
if (!cancelled) {
|
|
74
51
|
setUrl(fallbackUrl || null);
|
|
75
52
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -78,7 +55,7 @@ export const useFileDownloadUrl = (fileIdOrServices, fileIdOrOptions, maybeOptio
|
|
|
78
55
|
}
|
|
79
56
|
}
|
|
80
57
|
catch {
|
|
81
|
-
//
|
|
58
|
+
// Secondary failure: surface the original error below.
|
|
82
59
|
}
|
|
83
60
|
if (!cancelled) {
|
|
84
61
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -94,6 +71,6 @@ export const useFileDownloadUrl = (fileIdOrServices, fileIdOrOptions, maybeOptio
|
|
|
94
71
|
return () => {
|
|
95
72
|
cancelled = true;
|
|
96
73
|
};
|
|
97
|
-
}, [fileId,
|
|
74
|
+
}, [fileId, oxyServices, variant, expiresIn]);
|
|
98
75
|
return { url, loading, error };
|
|
99
76
|
};
|
|
@@ -24,7 +24,8 @@ function readTokenFromStorage() {
|
|
|
24
24
|
try {
|
|
25
25
|
return window.localStorage.getItem(LS_ACCESS_TOKEN_KEY);
|
|
26
26
|
}
|
|
27
|
-
catch {
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.warn('[oxy.session-socket] localStorage read failed:', err);
|
|
28
29
|
return null;
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -37,12 +38,12 @@ async function getSocketIO() {
|
|
|
37
38
|
return null;
|
|
38
39
|
_ioLoadAttempted = true;
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
_io = (mod.io ?? mod.default);
|
|
41
|
+
const mod = (await import('socket.io-client'));
|
|
42
|
+
_io = mod.io ?? mod.default ?? null;
|
|
43
43
|
return _io;
|
|
44
44
|
}
|
|
45
|
-
catch {
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.warn('[oxy.session-socket] socket.io-client import failed:', err);
|
|
46
47
|
debug.warn('socket.io-client is not installed. useSessionSocket will be disabled. Install it with: bun add socket.io-client');
|
|
47
48
|
return null;
|
|
48
49
|
}
|
|
@@ -109,7 +110,7 @@ export function useSessionSocket(options) {
|
|
|
109
110
|
// If no token is available at all, we skip the initial connect and let
|
|
110
111
|
// the storage listener or retry logic connect when a token appears.
|
|
111
112
|
const token = resolveToken();
|
|
112
|
-
|
|
113
|
+
const socket = ioFn(baseURL, {
|
|
113
114
|
transports: ['websocket'],
|
|
114
115
|
autoConnect: !!token, // don't auto-connect when there is no token
|
|
115
116
|
auth: (cb) => {
|
|
@@ -126,7 +127,7 @@ export function useSessionSocket(options) {
|
|
|
126
127
|
cb({ token: resolved });
|
|
127
128
|
},
|
|
128
129
|
});
|
|
129
|
-
|
|
130
|
+
socketRef.current = socket;
|
|
130
131
|
// Server auto-joins the user to `user:<userId>` room on connection
|
|
131
132
|
const handleConnect = () => {
|
|
132
133
|
debug.log('Socket connected:', socket.id);
|
|
@@ -162,110 +163,94 @@ export function useSessionSocket(options) {
|
|
|
162
163
|
invalidateSessionQueries(queryClientRef.current);
|
|
163
164
|
return Promise.resolve();
|
|
164
165
|
};
|
|
166
|
+
const triggerLocalSignOut = async (toastMessage, errorContext) => {
|
|
167
|
+
if (onRemoteSignOutRef.current) {
|
|
168
|
+
onRemoteSignOutRef.current();
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
toast.info(toastMessage);
|
|
172
|
+
}
|
|
173
|
+
// Clear local state since the server has already removed the session.
|
|
174
|
+
// Await so storage cleanup completes before any subsequent navigation.
|
|
175
|
+
try {
|
|
176
|
+
await clearSessionStateRef.current();
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
180
|
+
logger.error(`Failed to clear session state after ${errorContext}`, error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
165
184
|
const handleSessionUpdate = async (data) => {
|
|
166
185
|
debug.log('Received session_update:', data);
|
|
167
186
|
const currentActiveSessionId = activeSessionIdRef.current;
|
|
168
187
|
const deviceId = currentDeviceIdRef.current;
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
toast.info('You have been signed out remotely.');
|
|
188
|
+
// Strict whitelist. Every event type that may sign the user out must
|
|
189
|
+
// appear in the switch. Anything unknown falls through to `default`,
|
|
190
|
+
// which only logs in dev. This guards against future server-side event
|
|
191
|
+
// additions (e.g. `session_created` after a successful sign-in)
|
|
192
|
+
// accidentally triggering sign-out via a fallback branch that compares
|
|
193
|
+
// `data.sessionId === currentActiveSessionId` — that branch would match
|
|
194
|
+
// the user's NEW session id and trigger an instant remote sign-out
|
|
195
|
+
// toast on every login.
|
|
196
|
+
switch (data.type) {
|
|
197
|
+
case 'session_removed': {
|
|
198
|
+
if (data.sessionId && onSessionRemovedRef.current) {
|
|
199
|
+
onSessionRemovedRef.current(data.sessionId);
|
|
182
200
|
}
|
|
183
|
-
|
|
184
|
-
await
|
|
201
|
+
if (data.sessionId && data.sessionId === currentActiveSessionId) {
|
|
202
|
+
await triggerLocalSignOut('You have been signed out remotely.', 'session_removed');
|
|
185
203
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
logger.error('Failed to clear session state after session_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
189
|
-
}
|
|
204
|
+
else {
|
|
205
|
+
refreshSessions();
|
|
190
206
|
}
|
|
207
|
+
break;
|
|
191
208
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Track all removed sessions from this device
|
|
198
|
-
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
199
|
-
for (const sessionId of data.sessionIds) {
|
|
200
|
-
onSessionRemovedRef.current(sessionId);
|
|
209
|
+
case 'device_removed': {
|
|
210
|
+
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
211
|
+
for (const sessionId of data.sessionIds) {
|
|
212
|
+
onSessionRemovedRef.current(sessionId);
|
|
213
|
+
}
|
|
201
214
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (data.deviceId && data.deviceId === deviceId) {
|
|
205
|
-
if (onRemoteSignOutRef.current) {
|
|
206
|
-
onRemoteSignOutRef.current();
|
|
215
|
+
if (data.deviceId && deviceId && data.deviceId === deviceId) {
|
|
216
|
+
await triggerLocalSignOut('This device has been removed. You have been signed out.', 'device_removed');
|
|
207
217
|
}
|
|
208
218
|
else {
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
try {
|
|
212
|
-
await clearSessionStateRef.current();
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
216
|
-
logger.error('Failed to clear session state after device_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
217
|
-
}
|
|
219
|
+
refreshSessions();
|
|
218
220
|
}
|
|
221
|
+
break;
|
|
219
222
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// Track all removed sessions
|
|
226
|
-
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
227
|
-
for (const sessionId of data.sessionIds) {
|
|
228
|
-
onSessionRemovedRef.current(sessionId);
|
|
223
|
+
case 'sessions_removed': {
|
|
224
|
+
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
225
|
+
for (const sessionId of data.sessionIds) {
|
|
226
|
+
onSessionRemovedRef.current(sessionId);
|
|
227
|
+
}
|
|
229
228
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
onRemoteSignOutRef.current();
|
|
229
|
+
if (data.sessionIds &&
|
|
230
|
+
currentActiveSessionId &&
|
|
231
|
+
data.sessionIds.includes(currentActiveSessionId)) {
|
|
232
|
+
await triggerLocalSignOut('You have been signed out remotely.', 'sessions_removed');
|
|
235
233
|
}
|
|
236
234
|
else {
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
try {
|
|
240
|
-
await clearSessionStateRef.current();
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
243
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
244
|
-
logger.error('Failed to clear session state after sessions_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
245
|
-
}
|
|
235
|
+
refreshSessions();
|
|
246
236
|
}
|
|
237
|
+
break;
|
|
247
238
|
}
|
|
248
|
-
|
|
239
|
+
case 'session_created':
|
|
240
|
+
case 'session_update': {
|
|
241
|
+
// Lifecycle event for the current user. Just resync the sessions
|
|
242
|
+
// list — never sign out.
|
|
249
243
|
refreshSessions();
|
|
244
|
+
break;
|
|
250
245
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (onRemoteSignOutRef.current) {
|
|
258
|
-
onRemoteSignOutRef.current();
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
toast.info('You have been signed out remotely.');
|
|
262
|
-
}
|
|
263
|
-
try {
|
|
264
|
-
await clearSessionStateRef.current();
|
|
265
|
-
}
|
|
266
|
-
catch (error) {
|
|
267
|
-
debug.error('Failed to clear session state after session_update:', error);
|
|
246
|
+
default: {
|
|
247
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
248
|
+
logger.warn('Unknown session socket event type', {
|
|
249
|
+
component: 'useSessionSocket',
|
|
250
|
+
type: data.type,
|
|
251
|
+
});
|
|
268
252
|
}
|
|
253
|
+
break;
|
|
269
254
|
}
|
|
270
255
|
}
|
|
271
256
|
};
|
|
@@ -293,12 +278,14 @@ export function useSessionSocket(options) {
|
|
|
293
278
|
if (authRetryTimer) {
|
|
294
279
|
clearTimeout(authRetryTimer);
|
|
295
280
|
}
|
|
296
|
-
|
|
281
|
+
const currentSocket = socketRef.current;
|
|
282
|
+
if (currentSocket) {
|
|
297
283
|
// Remove cross-tab storage listener
|
|
298
|
-
|
|
299
|
-
|
|
284
|
+
const storageHandler = currentSocket.__oxyStorageHandler;
|
|
285
|
+
if (typeof window !== 'undefined' && storageHandler) {
|
|
286
|
+
window.removeEventListener('storage', storageHandler);
|
|
300
287
|
}
|
|
301
|
-
|
|
288
|
+
currentSocket.disconnect();
|
|
302
289
|
socketRef.current = null;
|
|
303
290
|
}
|
|
304
291
|
};
|
package/dist/esm/index.js
CHANGED
|
@@ -38,7 +38,7 @@ export { createProfileMutation, createGenericMutation, } from './hooks/mutations
|
|
|
38
38
|
export { useWebSSO, isWebBrowser } from './hooks/useWebSSO';
|
|
39
39
|
export { useSessionSocket } from './hooks/useSessionSocket';
|
|
40
40
|
export { useAssets, setOxyAssetInstance } from './hooks/useAssets';
|
|
41
|
-
export { useFileDownloadUrl
|
|
41
|
+
export { useFileDownloadUrl } from './hooks/useFileDownloadUrl';
|
|
42
42
|
export { useFollow, useFollowerCounts } from './hooks/useFollow';
|
|
43
43
|
export { useFileFiltering } from './hooks/useFileFiltering';
|
|
44
44
|
// --- Error Handlers ---
|
|
@@ -21,7 +21,9 @@ export const mapSessionsToClient = (sessions, fallbackDeviceId, fallbackUserId)
|
|
|
21
21
|
}));
|
|
22
22
|
};
|
|
23
23
|
/**
|
|
24
|
-
* Fetch device sessions
|
|
24
|
+
* Fetch device sessions, falling back to the per-user session endpoint
|
|
25
|
+
* if the device endpoint is unavailable (older API versions or disabled
|
|
26
|
+
* device-grouping feature flag).
|
|
25
27
|
*
|
|
26
28
|
* @param oxyServices - Oxy service instance
|
|
27
29
|
* @param sessionId - Session identifier to fetch
|
|
@@ -5,7 +5,8 @@ const MEMORY_STORAGE = () => {
|
|
|
5
5
|
const store = new Map();
|
|
6
6
|
return {
|
|
7
7
|
async getItem(key) {
|
|
8
|
-
|
|
8
|
+
const value = store.get(key);
|
|
9
|
+
return value === undefined ? null : value;
|
|
9
10
|
},
|
|
10
11
|
async setItem(key, value) {
|
|
11
12
|
store.set(key, value);
|
|
@@ -31,7 +32,8 @@ const createWebStorage = () => {
|
|
|
31
32
|
try {
|
|
32
33
|
return window.localStorage.getItem(key);
|
|
33
34
|
}
|
|
34
|
-
catch {
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.warn('[oxy.storage] localStorage.getItem failed:', err);
|
|
35
37
|
return null;
|
|
36
38
|
}
|
|
37
39
|
},
|
|
@@ -39,29 +41,44 @@ const createWebStorage = () => {
|
|
|
39
41
|
try {
|
|
40
42
|
window.localStorage.setItem(key, value);
|
|
41
43
|
}
|
|
42
|
-
catch {
|
|
43
|
-
//
|
|
44
|
+
catch (err) {
|
|
45
|
+
// Quota exceeded or storage disabled (e.g., Safari private mode).
|
|
46
|
+
// Surface to logs so it is debuggable, but do not throw so callers
|
|
47
|
+
// can keep functioning with degraded persistence.
|
|
48
|
+
console.warn('[oxy.storage] localStorage.setItem failed:', err);
|
|
44
49
|
}
|
|
45
50
|
},
|
|
46
51
|
async removeItem(key) {
|
|
47
52
|
try {
|
|
48
53
|
window.localStorage.removeItem(key);
|
|
49
54
|
}
|
|
50
|
-
catch {
|
|
51
|
-
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.warn('[oxy.storage] localStorage.removeItem failed:', err);
|
|
52
57
|
}
|
|
53
58
|
},
|
|
54
59
|
async clear() {
|
|
55
60
|
try {
|
|
56
61
|
window.localStorage.clear();
|
|
57
62
|
}
|
|
58
|
-
catch {
|
|
59
|
-
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.warn('[oxy.storage] localStorage.clear failed:', err);
|
|
60
65
|
}
|
|
61
66
|
},
|
|
62
67
|
};
|
|
63
68
|
};
|
|
64
69
|
let asyncStorageInstance = null;
|
|
70
|
+
/**
|
|
71
|
+
* Type guard verifying that an imported value exposes the AsyncStorage API.
|
|
72
|
+
*/
|
|
73
|
+
const isAsyncStorageLike = (value) => {
|
|
74
|
+
if (typeof value !== 'object' || value === null)
|
|
75
|
+
return false;
|
|
76
|
+
const candidate = value;
|
|
77
|
+
return (typeof candidate.getItem === 'function' &&
|
|
78
|
+
typeof candidate.setItem === 'function' &&
|
|
79
|
+
typeof candidate.removeItem === 'function' &&
|
|
80
|
+
typeof candidate.clear === 'function');
|
|
81
|
+
};
|
|
65
82
|
/**
|
|
66
83
|
* Lazily import React Native AsyncStorage implementation.
|
|
67
84
|
*/
|
|
@@ -72,8 +89,17 @@ const createNativeStorage = async () => {
|
|
|
72
89
|
try {
|
|
73
90
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
74
91
|
const moduleName = '@react-native-async-storage/async-storage';
|
|
75
|
-
const asyncStorageModule = await import(moduleName);
|
|
76
|
-
|
|
92
|
+
const asyncStorageModule = (await import(moduleName));
|
|
93
|
+
const candidate = asyncStorageModule.default;
|
|
94
|
+
if (!isAsyncStorageLike(candidate)) {
|
|
95
|
+
throw new Error('AsyncStorage default export does not match expected API');
|
|
96
|
+
}
|
|
97
|
+
asyncStorageInstance = {
|
|
98
|
+
getItem: (key) => candidate.getItem(key),
|
|
99
|
+
setItem: (key, value) => candidate.setItem(key, value),
|
|
100
|
+
removeItem: (key) => candidate.removeItem(key),
|
|
101
|
+
clear: () => candidate.clear(),
|
|
102
|
+
};
|
|
77
103
|
return asyncStorageInstance;
|
|
78
104
|
}
|
|
79
105
|
catch (error) {
|