@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
|
@@ -27,12 +27,40 @@ function readTokenFromStorage(): string | null {
|
|
|
27
27
|
if (typeof window === 'undefined') return null;
|
|
28
28
|
try {
|
|
29
29
|
return window.localStorage.getItem(LS_ACCESS_TOKEN_KEY);
|
|
30
|
-
} catch {
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.warn('[oxy.session-socket] localStorage read failed:', err);
|
|
31
32
|
return null;
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Minimal subset of the socket.io-client Socket API used by this hook.
|
|
38
|
+
* We avoid importing socket.io-client types directly because the package
|
|
39
|
+
* is an optional peer dependency.
|
|
40
|
+
*
|
|
41
|
+
* `on()` uses a generic per-call handler signature because each socket event
|
|
42
|
+
* carries its own payload shape.
|
|
43
|
+
*/
|
|
44
|
+
interface MinimalSocket {
|
|
45
|
+
id?: string;
|
|
46
|
+
disconnected: boolean;
|
|
47
|
+
connect: () => void;
|
|
48
|
+
disconnect: () => void;
|
|
49
|
+
on<Args extends unknown[] = unknown[]>(
|
|
50
|
+
event: string,
|
|
51
|
+
handler: (...args: Args) => void
|
|
52
|
+
): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Socket extended with a private property used to track the cross-tab
|
|
57
|
+
* storage event listener so cleanup can remove it.
|
|
58
|
+
*/
|
|
59
|
+
interface SocketWithStorageHandler extends MinimalSocket {
|
|
60
|
+
__oxyStorageHandler?: (event: StorageEvent) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type SocketIOFactory = (uri: string, opts?: Record<string, unknown>) => MinimalSocket;
|
|
36
64
|
|
|
37
65
|
let _io: SocketIOFactory | null = null;
|
|
38
66
|
let _ioLoadAttempted = false;
|
|
@@ -42,11 +70,14 @@ async function getSocketIO(): Promise<SocketIOFactory | null> {
|
|
|
42
70
|
if (_ioLoadAttempted) return null;
|
|
43
71
|
_ioLoadAttempted = true;
|
|
44
72
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
const mod = (await import('socket.io-client')) as {
|
|
74
|
+
io?: SocketIOFactory;
|
|
75
|
+
default?: SocketIOFactory;
|
|
76
|
+
};
|
|
77
|
+
_io = mod.io ?? mod.default ?? null;
|
|
48
78
|
return _io;
|
|
49
|
-
} catch {
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.warn('[oxy.session-socket] socket.io-client import failed:', err);
|
|
50
81
|
debug.warn('socket.io-client is not installed. useSessionSocket will be disabled. Install it with: bun add socket.io-client');
|
|
51
82
|
return null;
|
|
52
83
|
}
|
|
@@ -72,7 +103,7 @@ export function useSessionSocket(options?: UseSessionSocketOptions) {
|
|
|
72
103
|
return active?.deviceId ?? null;
|
|
73
104
|
}, [sessions, activeSessionId]);
|
|
74
105
|
|
|
75
|
-
const socketRef = useRef<
|
|
106
|
+
const socketRef = useRef<SocketWithStorageHandler | null>(null);
|
|
76
107
|
|
|
77
108
|
// Store callbacks and values in refs to avoid reconnecting when they change
|
|
78
109
|
const clearSessionStateRef = useRef(clearSessionState);
|
|
@@ -128,7 +159,7 @@ export function useSessionSocket(options?: UseSessionSocketOptions) {
|
|
|
128
159
|
// If no token is available at all, we skip the initial connect and let
|
|
129
160
|
// the storage listener or retry logic connect when a token appears.
|
|
130
161
|
const token = resolveToken();
|
|
131
|
-
|
|
162
|
+
const socket: SocketWithStorageHandler = ioFn(baseURL, {
|
|
132
163
|
transports: ['websocket'],
|
|
133
164
|
autoConnect: !!token, // don't auto-connect when there is no token
|
|
134
165
|
auth: (cb: (data: { token: string }) => void) => {
|
|
@@ -145,7 +176,7 @@ export function useSessionSocket(options?: UseSessionSocketOptions) {
|
|
|
145
176
|
cb({ token: resolved });
|
|
146
177
|
},
|
|
147
178
|
});
|
|
148
|
-
|
|
179
|
+
socketRef.current = socket;
|
|
149
180
|
|
|
150
181
|
// Server auto-joins the user to `user:<userId>` room on connection
|
|
151
182
|
const handleConnect = () => {
|
|
@@ -188,6 +219,27 @@ export function useSessionSocket(options?: UseSessionSocketOptions) {
|
|
|
188
219
|
return Promise.resolve();
|
|
189
220
|
};
|
|
190
221
|
|
|
222
|
+
const triggerLocalSignOut = async (toastMessage: string, errorContext: string) => {
|
|
223
|
+
if (onRemoteSignOutRef.current) {
|
|
224
|
+
onRemoteSignOutRef.current();
|
|
225
|
+
} else {
|
|
226
|
+
toast.info(toastMessage);
|
|
227
|
+
}
|
|
228
|
+
// Clear local state since the server has already removed the session.
|
|
229
|
+
// Await so storage cleanup completes before any subsequent navigation.
|
|
230
|
+
try {
|
|
231
|
+
await clearSessionStateRef.current();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
234
|
+
logger.error(
|
|
235
|
+
`Failed to clear session state after ${errorContext}`,
|
|
236
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
237
|
+
{ component: 'useSessionSocket' },
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
191
243
|
const handleSessionUpdate = async (data: {
|
|
192
244
|
type: string;
|
|
193
245
|
sessionId?: string;
|
|
@@ -199,96 +251,74 @@ export function useSessionSocket(options?: UseSessionSocketOptions) {
|
|
|
199
251
|
const currentActiveSessionId = activeSessionIdRef.current;
|
|
200
252
|
const deviceId = currentDeviceIdRef.current;
|
|
201
253
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
toast.info('You have been signed out remotely.');
|
|
254
|
+
// Strict whitelist. Every event type that may sign the user out must
|
|
255
|
+
// appear in the switch. Anything unknown falls through to `default`,
|
|
256
|
+
// which only logs in dev. This guards against future server-side event
|
|
257
|
+
// additions (e.g. `session_created` after a successful sign-in)
|
|
258
|
+
// accidentally triggering sign-out via a fallback branch that compares
|
|
259
|
+
// `data.sessionId === currentActiveSessionId` — that branch would match
|
|
260
|
+
// the user's NEW session id and trigger an instant remote sign-out
|
|
261
|
+
// toast on every login.
|
|
262
|
+
switch (data.type) {
|
|
263
|
+
case 'session_removed': {
|
|
264
|
+
if (data.sessionId && onSessionRemovedRef.current) {
|
|
265
|
+
onSessionRemovedRef.current(data.sessionId);
|
|
215
266
|
}
|
|
216
|
-
|
|
217
|
-
await
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
logger.error('Failed to clear session state after session_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
221
|
-
}
|
|
267
|
+
if (data.sessionId && data.sessionId === currentActiveSessionId) {
|
|
268
|
+
await triggerLocalSignOut('You have been signed out remotely.', 'session_removed');
|
|
269
|
+
} else {
|
|
270
|
+
refreshSessions();
|
|
222
271
|
}
|
|
223
|
-
|
|
224
|
-
refreshSessions();
|
|
272
|
+
break;
|
|
225
273
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
274
|
+
case 'device_removed': {
|
|
275
|
+
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
276
|
+
for (const sessionId of data.sessionIds) {
|
|
277
|
+
onSessionRemovedRef.current(sessionId);
|
|
278
|
+
}
|
|
231
279
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
onRemoteSignOutRef.current();
|
|
280
|
+
if (data.deviceId && deviceId && data.deviceId === deviceId) {
|
|
281
|
+
await triggerLocalSignOut(
|
|
282
|
+
'This device has been removed. You have been signed out.',
|
|
283
|
+
'device_removed',
|
|
284
|
+
);
|
|
238
285
|
} else {
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
try {
|
|
242
|
-
await clearSessionStateRef.current();
|
|
243
|
-
} catch (error) {
|
|
244
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
245
|
-
logger.error('Failed to clear session state after device_removed', error instanceof Error ? error : new Error(String(error)), { component: 'useSessionSocket' });
|
|
246
|
-
}
|
|
286
|
+
refreshSessions();
|
|
247
287
|
}
|
|
248
|
-
|
|
249
|
-
refreshSessions();
|
|
288
|
+
break;
|
|
250
289
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
290
|
+
case 'sessions_removed': {
|
|
291
|
+
if (data.sessionIds && onSessionRemovedRef.current) {
|
|
292
|
+
for (const sessionId of data.sessionIds) {
|
|
293
|
+
onSessionRemovedRef.current(sessionId);
|
|
294
|
+
}
|
|
256
295
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
296
|
+
if (
|
|
297
|
+
data.sessionIds &&
|
|
298
|
+
currentActiveSessionId &&
|
|
299
|
+
data.sessionIds.includes(currentActiveSessionId)
|
|
300
|
+
) {
|
|
301
|
+
await triggerLocalSignOut('You have been signed out remotely.', 'sessions_removed');
|
|
263
302
|
} else {
|
|
264
|
-
|
|
303
|
+
refreshSessions();
|
|
265
304
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
} else {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case 'session_created':
|
|
308
|
+
case 'session_update': {
|
|
309
|
+
// Lifecycle event for the current user. Just resync the sessions
|
|
310
|
+
// list — never sign out.
|
|
274
311
|
refreshSessions();
|
|
312
|
+
break;
|
|
275
313
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (onRemoteSignOutRef.current) {
|
|
283
|
-
onRemoteSignOutRef.current();
|
|
284
|
-
} else {
|
|
285
|
-
toast.info('You have been signed out remotely.');
|
|
286
|
-
}
|
|
287
|
-
try {
|
|
288
|
-
await clearSessionStateRef.current();
|
|
289
|
-
} catch (error) {
|
|
290
|
-
debug.error('Failed to clear session state after session_update:', error);
|
|
314
|
+
default: {
|
|
315
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
316
|
+
logger.warn('Unknown session socket event type', {
|
|
317
|
+
component: 'useSessionSocket',
|
|
318
|
+
type: data.type,
|
|
319
|
+
});
|
|
291
320
|
}
|
|
321
|
+
break;
|
|
292
322
|
}
|
|
293
323
|
}
|
|
294
324
|
};
|
|
@@ -311,7 +341,7 @@ export function useSessionSocket(options?: UseSessionSocketOptions) {
|
|
|
311
341
|
if (typeof window !== 'undefined') {
|
|
312
342
|
window.addEventListener('storage', handleStorageEvent);
|
|
313
343
|
// Store the handler so cleanup can remove it
|
|
314
|
-
|
|
344
|
+
socket.__oxyStorageHandler = handleStorageEvent;
|
|
315
345
|
}
|
|
316
346
|
});
|
|
317
347
|
|
|
@@ -320,12 +350,14 @@ export function useSessionSocket(options?: UseSessionSocketOptions) {
|
|
|
320
350
|
if (authRetryTimer) {
|
|
321
351
|
clearTimeout(authRetryTimer);
|
|
322
352
|
}
|
|
323
|
-
|
|
353
|
+
const currentSocket = socketRef.current;
|
|
354
|
+
if (currentSocket) {
|
|
324
355
|
// Remove cross-tab storage listener
|
|
325
|
-
|
|
326
|
-
|
|
356
|
+
const storageHandler = currentSocket.__oxyStorageHandler;
|
|
357
|
+
if (typeof window !== 'undefined' && storageHandler) {
|
|
358
|
+
window.removeEventListener('storage', storageHandler);
|
|
327
359
|
}
|
|
328
|
-
|
|
360
|
+
currentSocket.disconnect();
|
|
329
361
|
socketRef.current = null;
|
|
330
362
|
}
|
|
331
363
|
};
|
package/src/index.ts
CHANGED
|
@@ -74,6 +74,10 @@ export {
|
|
|
74
74
|
useSecurityInfo,
|
|
75
75
|
useSecurityActivity,
|
|
76
76
|
useRecentSecurityActivity,
|
|
77
|
+
useAppData,
|
|
78
|
+
useAppDataNamespace,
|
|
79
|
+
appDataQueryKeys,
|
|
80
|
+
isMissingAppDataEndpointError,
|
|
77
81
|
} from './hooks/queries';
|
|
78
82
|
|
|
79
83
|
// --- Mutation Hooks ---
|
|
@@ -88,6 +92,8 @@ export {
|
|
|
88
92
|
useLogoutAll,
|
|
89
93
|
useUpdateDeviceName,
|
|
90
94
|
useRemoveDevice,
|
|
95
|
+
useSetAppData,
|
|
96
|
+
useDeleteAppData,
|
|
91
97
|
} from './hooks/mutations';
|
|
92
98
|
|
|
93
99
|
export {
|
|
@@ -104,7 +110,7 @@ export { useWebSSO, isWebBrowser } from './hooks/useWebSSO';
|
|
|
104
110
|
export { useSessionSocket } from './hooks/useSessionSocket';
|
|
105
111
|
export type { UseSessionSocketOptions } from './hooks/useSessionSocket';
|
|
106
112
|
export { useAssets, setOxyAssetInstance } from './hooks/useAssets';
|
|
107
|
-
export { useFileDownloadUrl
|
|
113
|
+
export { useFileDownloadUrl } from './hooks/useFileDownloadUrl';
|
|
108
114
|
export { useFollow, useFollowerCounts } from './hooks/useFollow';
|
|
109
115
|
export { useFileFiltering } from './hooks/useFileFiltering';
|
|
110
116
|
export type { ViewMode, SortBy, SortOrder } from './hooks/useFileFiltering';
|
|
@@ -68,7 +68,9 @@ export const mapSessionsToClient = (
|
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Fetch device sessions
|
|
71
|
+
* Fetch device sessions, falling back to the per-user session endpoint
|
|
72
|
+
* if the device endpoint is unavailable (older API versions or disabled
|
|
73
|
+
* device-grouping feature flag).
|
|
72
74
|
*
|
|
73
75
|
* @param oxyServices - Oxy service instance
|
|
74
76
|
* @param sessionId - Session identifier to fetch
|
|
@@ -19,7 +19,8 @@ const MEMORY_STORAGE = (): StorageInterface => {
|
|
|
19
19
|
|
|
20
20
|
return {
|
|
21
21
|
async getItem(key: string) {
|
|
22
|
-
|
|
22
|
+
const value = store.get(key);
|
|
23
|
+
return value === undefined ? null : value;
|
|
23
24
|
},
|
|
24
25
|
async setItem(key: string, value: string) {
|
|
25
26
|
store.set(key, value);
|
|
@@ -46,29 +47,33 @@ const createWebStorage = (): StorageInterface => {
|
|
|
46
47
|
async getItem(key: string) {
|
|
47
48
|
try {
|
|
48
49
|
return window.localStorage.getItem(key);
|
|
49
|
-
} catch {
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.warn('[oxy.storage] localStorage.getItem failed:', err);
|
|
50
52
|
return null;
|
|
51
53
|
}
|
|
52
54
|
},
|
|
53
55
|
async setItem(key: string, value: string) {
|
|
54
56
|
try {
|
|
55
57
|
window.localStorage.setItem(key, value);
|
|
56
|
-
} catch {
|
|
57
|
-
//
|
|
58
|
+
} catch (err) {
|
|
59
|
+
// Quota exceeded or storage disabled (e.g., Safari private mode).
|
|
60
|
+
// Surface to logs so it is debuggable, but do not throw so callers
|
|
61
|
+
// can keep functioning with degraded persistence.
|
|
62
|
+
console.warn('[oxy.storage] localStorage.setItem failed:', err);
|
|
58
63
|
}
|
|
59
64
|
},
|
|
60
65
|
async removeItem(key: string) {
|
|
61
66
|
try {
|
|
62
67
|
window.localStorage.removeItem(key);
|
|
63
|
-
} catch {
|
|
64
|
-
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.warn('[oxy.storage] localStorage.removeItem failed:', err);
|
|
65
70
|
}
|
|
66
71
|
},
|
|
67
72
|
async clear() {
|
|
68
73
|
try {
|
|
69
74
|
window.localStorage.clear();
|
|
70
|
-
} catch {
|
|
71
|
-
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.warn('[oxy.storage] localStorage.clear failed:', err);
|
|
72
77
|
}
|
|
73
78
|
},
|
|
74
79
|
};
|
|
@@ -76,6 +81,31 @@ const createWebStorage = (): StorageInterface => {
|
|
|
76
81
|
|
|
77
82
|
let asyncStorageInstance: StorageInterface | null = null;
|
|
78
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Structural type for the React Native AsyncStorage default export.
|
|
86
|
+
* Only includes the methods this SDK uses.
|
|
87
|
+
*/
|
|
88
|
+
interface AsyncStorageLike {
|
|
89
|
+
getItem: (key: string) => Promise<string | null>;
|
|
90
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
91
|
+
removeItem: (key: string) => Promise<void>;
|
|
92
|
+
clear: () => Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Type guard verifying that an imported value exposes the AsyncStorage API.
|
|
97
|
+
*/
|
|
98
|
+
const isAsyncStorageLike = (value: unknown): value is AsyncStorageLike => {
|
|
99
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
100
|
+
const candidate = value as Record<string, unknown>;
|
|
101
|
+
return (
|
|
102
|
+
typeof candidate.getItem === 'function' &&
|
|
103
|
+
typeof candidate.setItem === 'function' &&
|
|
104
|
+
typeof candidate.removeItem === 'function' &&
|
|
105
|
+
typeof candidate.clear === 'function'
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
79
109
|
/**
|
|
80
110
|
* Lazily import React Native AsyncStorage implementation.
|
|
81
111
|
*/
|
|
@@ -87,8 +117,17 @@ const createNativeStorage = async (): Promise<StorageInterface> => {
|
|
|
87
117
|
try {
|
|
88
118
|
// Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
|
|
89
119
|
const moduleName = '@react-native-async-storage/async-storage';
|
|
90
|
-
const asyncStorageModule = await import(moduleName);
|
|
91
|
-
|
|
120
|
+
const asyncStorageModule = (await import(moduleName)) as { default?: unknown };
|
|
121
|
+
const candidate = asyncStorageModule.default;
|
|
122
|
+
if (!isAsyncStorageLike(candidate)) {
|
|
123
|
+
throw new Error('AsyncStorage default export does not match expected API');
|
|
124
|
+
}
|
|
125
|
+
asyncStorageInstance = {
|
|
126
|
+
getItem: (key) => candidate.getItem(key),
|
|
127
|
+
setItem: (key, value) => candidate.setItem(key, value),
|
|
128
|
+
removeItem: (key) => candidate.removeItem(key),
|
|
129
|
+
clear: () => candidate.clear(),
|
|
130
|
+
};
|
|
92
131
|
return asyncStorageInstance;
|
|
93
132
|
} catch (error) {
|
|
94
133
|
if (process.env.NODE_ENV !== 'production') {
|