@oxyhq/core 1.0.1 → 1.1.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.
Files changed (92) hide show
  1. package/dist/cjs/AuthManager.js +19 -9
  2. package/dist/cjs/CrossDomainAuth.js +2 -2
  3. package/dist/cjs/HttpService.js +9 -8
  4. package/dist/cjs/OxyServices.base.js +16 -3
  5. package/dist/cjs/crypto/keyManager.js +29 -24
  6. package/dist/cjs/crypto/polyfill.js +6 -1
  7. package/dist/cjs/crypto/signatureService.js +40 -31
  8. package/dist/cjs/i18n/index.js +36 -45
  9. package/dist/cjs/i18n/locales/ar-SA.json +114 -115
  10. package/dist/cjs/i18n/locales/ca-ES.json +114 -115
  11. package/dist/cjs/i18n/locales/de-DE.json +114 -115
  12. package/dist/cjs/i18n/locales/en-US.json +936 -936
  13. package/dist/cjs/i18n/locales/es-ES.json +924 -924
  14. package/dist/cjs/i18n/locales/fr-FR.json +114 -115
  15. package/dist/cjs/i18n/locales/it-IT.json +114 -115
  16. package/dist/cjs/i18n/locales/ja-JP.json +2 -2
  17. package/dist/cjs/i18n/locales/ko-KR.json +114 -115
  18. package/dist/cjs/i18n/locales/pt-PT.json +114 -115
  19. package/dist/cjs/i18n/locales/zh-CN.json +114 -115
  20. package/dist/cjs/mixins/OxyServices.fedcm.js +13 -41
  21. package/dist/cjs/mixins/OxyServices.language.js +5 -2
  22. package/dist/cjs/mixins/OxyServices.privacy.js +2 -1
  23. package/dist/cjs/mixins/OxyServices.security.js +3 -2
  24. package/dist/cjs/shared/utils/debugUtils.js +8 -1
  25. package/dist/cjs/utils/deviceManager.js +3 -1
  26. package/dist/cjs/utils/platform.js +3 -2
  27. package/dist/esm/AuthManager.js +19 -9
  28. package/dist/esm/CrossDomainAuth.js +2 -2
  29. package/dist/esm/HttpService.js +9 -8
  30. package/dist/esm/OxyServices.base.js +16 -3
  31. package/dist/esm/crypto/keyManager.js +29 -24
  32. package/dist/esm/crypto/polyfill.js +6 -1
  33. package/dist/esm/crypto/signatureService.js +40 -31
  34. package/dist/esm/i18n/index.js +11 -23
  35. package/dist/esm/i18n/locales/ar-SA.json +114 -115
  36. package/dist/esm/i18n/locales/ca-ES.json +114 -115
  37. package/dist/esm/i18n/locales/de-DE.json +114 -115
  38. package/dist/esm/i18n/locales/en-US.json +936 -936
  39. package/dist/esm/i18n/locales/es-ES.json +924 -924
  40. package/dist/esm/i18n/locales/fr-FR.json +114 -115
  41. package/dist/esm/i18n/locales/it-IT.json +114 -115
  42. package/dist/esm/i18n/locales/ja-JP.json +2 -2
  43. package/dist/esm/i18n/locales/ko-KR.json +114 -115
  44. package/dist/esm/i18n/locales/pt-PT.json +114 -115
  45. package/dist/esm/i18n/locales/zh-CN.json +114 -115
  46. package/dist/esm/mixins/OxyServices.fedcm.js +13 -41
  47. package/dist/esm/mixins/OxyServices.language.js +5 -2
  48. package/dist/esm/mixins/OxyServices.privacy.js +2 -1
  49. package/dist/esm/mixins/OxyServices.security.js +3 -2
  50. package/dist/esm/shared/utils/debugUtils.js +8 -1
  51. package/dist/esm/utils/deviceManager.js +3 -1
  52. package/dist/esm/utils/platform.js +3 -2
  53. package/dist/types/CrossDomainAuth.d.ts +2 -2
  54. package/dist/types/OxyServices.base.d.ts +4 -1
  55. package/dist/types/OxyServices.d.ts +13 -0
  56. package/dist/types/index.d.ts +3 -0
  57. package/dist/types/mixins/OxyServices.analytics.d.ts +2 -0
  58. package/dist/types/mixins/OxyServices.assets.d.ts +2 -0
  59. package/dist/types/mixins/OxyServices.auth.d.ts +2 -0
  60. package/dist/types/mixins/OxyServices.developer.d.ts +2 -0
  61. package/dist/types/mixins/OxyServices.devices.d.ts +2 -0
  62. package/dist/types/mixins/OxyServices.features.d.ts +2 -0
  63. package/dist/types/mixins/OxyServices.fedcm.d.ts +4 -2
  64. package/dist/types/mixins/OxyServices.karma.d.ts +2 -0
  65. package/dist/types/mixins/OxyServices.language.d.ts +2 -0
  66. package/dist/types/mixins/OxyServices.location.d.ts +2 -0
  67. package/dist/types/mixins/OxyServices.payment.d.ts +2 -0
  68. package/dist/types/mixins/OxyServices.popup.d.ts +2 -0
  69. package/dist/types/mixins/OxyServices.privacy.d.ts +2 -0
  70. package/dist/types/mixins/OxyServices.redirect.d.ts +2 -0
  71. package/dist/types/mixins/OxyServices.security.d.ts +2 -0
  72. package/dist/types/mixins/OxyServices.user.d.ts +2 -0
  73. package/dist/types/mixins/OxyServices.utility.d.ts +2 -0
  74. package/package.json +2 -3
  75. package/src/AuthManager.ts +25 -15
  76. package/src/CrossDomainAuth.ts +2 -2
  77. package/src/HttpService.ts +9 -8
  78. package/src/OxyServices.base.ts +21 -4
  79. package/src/OxyServices.ts +23 -2
  80. package/src/crypto/keyManager.ts +30 -25
  81. package/src/crypto/polyfill.ts +6 -1
  82. package/src/crypto/signatureService.ts +43 -37
  83. package/src/i18n/index.ts +33 -45
  84. package/src/index.ts +3 -0
  85. package/src/mixins/OxyServices.fedcm.ts +14 -44
  86. package/src/mixins/OxyServices.language.ts +6 -3
  87. package/src/mixins/OxyServices.privacy.ts +2 -1
  88. package/src/mixins/OxyServices.security.ts +3 -2
  89. package/src/shared/utils/__tests__/debugUtils.test.ts +55 -0
  90. package/src/shared/utils/debugUtils.ts +6 -1
  91. package/src/utils/deviceManager.ts +4 -2
  92. package/src/utils/platform.ts +3 -2
package/src/i18n/index.ts CHANGED
@@ -1,52 +1,40 @@
1
- // Use JSON locale files (RN Metro supports static requires reliably)
2
- // eslint-disable-next-line @typescript-eslint/no-var-requires
3
- const enUS = require('./locales/en-US.json') as Record<string, any>;
4
- // eslint-disable-next-line @typescript-eslint/no-var-requires
5
- const esES = require('./locales/es-ES.json') as Record<string, any>;
6
- // eslint-disable-next-line @typescript-eslint/no-var-requires
7
- const caES = require('./locales/ca-ES.json') as Record<string, any>;
8
- // eslint-disable-next-line @typescript-eslint/no-var-requires
9
- const frFR = require('./locales/fr-FR.json') as Record<string, any>;
10
- // eslint-disable-next-line @typescript-eslint/no-var-requires
11
- const deDE = require('./locales/de-DE.json') as Record<string, any>;
12
- // eslint-disable-next-line @typescript-eslint/no-var-requires
13
- const itIT = require('./locales/it-IT.json') as Record<string, any>;
14
- // eslint-disable-next-line @typescript-eslint/no-var-requires
15
- const ptPT = require('./locales/pt-PT.json') as Record<string, any>;
16
- // eslint-disable-next-line @typescript-eslint/no-var-requires
17
- const jaJP = require('./locales/ja-JP.json') as Record<string, any>;
18
- // eslint-disable-next-line @typescript-eslint/no-var-requires
19
- const koKR = require('./locales/ko-KR.json') as Record<string, any>;
20
- // eslint-disable-next-line @typescript-eslint/no-var-requires
21
- const zhCN = require('./locales/zh-CN.json') as Record<string, any>;
22
- // eslint-disable-next-line @typescript-eslint/no-var-requires
23
- const arSA = require('./locales/ar-SA.json') as Record<string, any>;
1
+ import enUS from './locales/en-US.json';
2
+ import esES from './locales/es-ES.json';
3
+ import caES from './locales/ca-ES.json';
4
+ import frFR from './locales/fr-FR.json';
5
+ import deDE from './locales/de-DE.json';
6
+ import itIT from './locales/it-IT.json';
7
+ import ptPT from './locales/pt-PT.json';
8
+ import jaJP from './locales/ja-JP.json';
9
+ import koKR from './locales/ko-KR.json';
10
+ import zhCN from './locales/zh-CN.json';
11
+ import arSA from './locales/ar-SA.json';
24
12
 
25
13
  export type LocaleDict = Record<string, any>;
26
14
 
27
15
  const DICTS: Record<string, LocaleDict> = {
28
- 'en': enUS as LocaleDict,
29
- 'en-US': enUS as LocaleDict,
30
- 'es': esES as LocaleDict,
31
- 'es-ES': esES as LocaleDict,
32
- 'ca': caES as LocaleDict,
33
- 'ca-ES': caES as LocaleDict,
34
- 'fr': frFR as LocaleDict,
35
- 'fr-FR': frFR as LocaleDict,
36
- 'de': deDE as LocaleDict,
37
- 'de-DE': deDE as LocaleDict,
38
- 'it': itIT as LocaleDict,
39
- 'it-IT': itIT as LocaleDict,
40
- 'pt': ptPT as LocaleDict,
41
- 'pt-PT': ptPT as LocaleDict,
42
- 'ja': jaJP as LocaleDict,
43
- 'ja-JP': jaJP as LocaleDict,
44
- 'ko': koKR as LocaleDict,
45
- 'ko-KR': koKR as LocaleDict,
46
- 'zh': zhCN as LocaleDict,
47
- 'zh-CN': zhCN as LocaleDict,
48
- 'ar': arSA as LocaleDict,
49
- 'ar-SA': arSA as LocaleDict,
16
+ 'en': enUS,
17
+ 'en-US': enUS,
18
+ 'es': esES,
19
+ 'es-ES': esES,
20
+ 'ca': caES,
21
+ 'ca-ES': caES,
22
+ 'fr': frFR,
23
+ 'fr-FR': frFR,
24
+ 'de': deDE,
25
+ 'de-DE': deDE,
26
+ 'it': itIT,
27
+ 'it-IT': itIT,
28
+ 'pt': ptPT,
29
+ 'pt-PT': ptPT,
30
+ 'ja': jaJP,
31
+ 'ja-JP': jaJP,
32
+ 'ko': koKR,
33
+ 'ko-KR': koKR,
34
+ 'zh': zhCN,
35
+ 'zh-CN': zhCN,
36
+ 'ar': arSA,
37
+ 'ar-SA': arSA,
50
38
  };
51
39
 
52
40
  const FALLBACK = 'en-US';
package/src/index.ts CHANGED
@@ -27,6 +27,9 @@ export type { StorageAdapter, AuthStateChangeCallback, AuthMethod, AuthManagerCo
27
27
 
28
28
  export { CrossDomainAuth, createCrossDomainAuth } from './CrossDomainAuth';
29
29
  export type { CrossDomainAuthOptions } from './CrossDomainAuth';
30
+ export type { FedCMAuthOptions, FedCMConfig } from './mixins/OxyServices.fedcm';
31
+ export type { PopupAuthOptions } from './mixins/OxyServices.popup';
32
+ export type { RedirectAuthOptions } from './mixins/OxyServices.redirect';
30
33
 
31
34
  // --- Crypto / Identity ---
32
35
  export { KeyManager, SignatureService, RecoveryPhraseService } from './crypto';
@@ -49,8 +49,8 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
49
49
  super(...(args as [any]));
50
50
  }
51
51
  public static readonly DEFAULT_CONFIG_URL = 'https://auth.oxy.so/fedcm.json';
52
- public static readonly FEDCM_TIMEOUT = 60000; // 1 minute for interactive
53
- public static readonly FEDCM_SILENT_TIMEOUT = 10000; // 10 seconds for silent mediation
52
+ public static readonly FEDCM_TIMEOUT = 15000; // 15 seconds for interactive
53
+ public static readonly FEDCM_SILENT_TIMEOUT = 3000; // 3 seconds for silent mediation
54
54
 
55
55
  /**
56
56
  * Check if FedCM is supported in the current browser
@@ -180,7 +180,10 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
180
180
  const clientId = this.getClientId();
181
181
  debug.log('Silent SSO: Starting for', clientId);
182
182
 
183
- // First try silent mediation (no UI) - works if user previously consented
183
+ // Only try silent mediation (no UI) - works if user previously consented.
184
+ // We intentionally do NOT fall back to optional mediation here because
185
+ // this runs on app startup — showing browser UI without user action is bad UX.
186
+ // Optional/interactive mediation should only happen when the user clicks "Sign In".
184
187
  let credential: { token: string } | null = null;
185
188
 
186
189
  try {
@@ -196,36 +199,14 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
196
199
 
197
200
  debug.log('Silent SSO: Silent mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
198
201
  } catch (silentError) {
199
- // Silent mediation failed - this is expected if user hasn't consented before or is in quiet period
200
202
  const errorName = silentError instanceof Error ? silentError.name : 'Unknown';
201
203
  const errorMessage = silentError instanceof Error ? silentError.message : String(silentError);
202
- debug.log('Silent SSO: Silent mediation error (will try optional):', { name: errorName, message: errorMessage });
203
- }
204
-
205
- // If silent failed, try optional mediation which shows browser UI if needed
206
- if (!credential || !credential.token) {
207
- try {
208
- const nonce = this.generateNonce();
209
- debug.log('Silent SSO: Trying optional mediation (may show browser UI)...');
210
-
211
- credential = await this.requestIdentityCredential({
212
- configURL: (this.constructor as any).DEFAULT_CONFIG_URL,
213
- clientId,
214
- nonce,
215
- mediation: 'optional',
216
- });
217
-
218
- debug.log('Silent SSO: Optional mediation result:', { hasCredential: !!credential, hasToken: !!credential?.token });
219
- } catch (optionalError) {
220
- const errorName = optionalError instanceof Error ? optionalError.name : 'Unknown';
221
- const errorMessage = optionalError instanceof Error ? optionalError.message : String(optionalError);
222
- debug.log('Silent SSO: Optional mediation also failed:', { name: errorName, message: errorMessage });
223
- return null;
224
- }
204
+ debug.log('Silent SSO: Silent mediation failed:', { name: errorName, message: errorMessage });
205
+ return null;
225
206
  }
226
207
 
227
208
  if (!credential || !credential.token) {
228
- debug.log('Silent SSO: No credential returned (user may have dismissed prompt or is not logged in at IdP)');
209
+ debug.log('Silent SSO: No credential returned (user not logged in at IdP or hasn\'t consented)');
229
210
  return null;
230
211
  }
231
212
 
@@ -392,9 +373,7 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
392
373
  * @private
393
374
  */
394
375
  public async exchangeIdTokenForSession(idToken: string): Promise<SessionLoginResponse> {
395
- debug.log('exchangeIdTokenForSession: Starting exchange...');
396
- debug.log('exchangeIdTokenForSession: Token length:', idToken?.length);
397
- debug.log('exchangeIdTokenForSession: Token preview:', idToken?.substring(0, 50) + '...');
376
+ debug.log('Exchanging ID token for session...');
398
377
 
399
378
  try {
400
379
  const response = await this.makeRequest<SessionLoginResponse>(
@@ -404,23 +383,14 @@ export function OxyServicesFedCMMixin<T extends typeof OxyServicesBase>(Base: T)
404
383
  { cache: false }
405
384
  );
406
385
 
407
- debug.log('exchangeIdTokenForSession: Response received:', {
408
- hasResponse: !!response,
409
- hasSessionId: !!(response as any)?.sessionId,
410
- hasUser: !!(response as any)?.user,
411
- hasAccessToken: !!(response as any)?.accessToken,
412
- userId: (response as any)?.user?.id,
413
- username: (response as any)?.user?.username,
414
- responseKeys: response ? Object.keys(response) : [],
386
+ debug.log('Token exchange complete:', {
387
+ hasSession: !!response?.sessionId,
388
+ hasUser: !!response?.user,
415
389
  });
416
390
 
417
391
  return response;
418
392
  } catch (error) {
419
- debug.error('exchangeIdTokenForSession: Error:', {
420
- name: error instanceof Error ? error.name : 'Unknown',
421
- message: error instanceof Error ? error.message : String(error),
422
- stack: error instanceof Error ? error.stack : undefined,
423
- });
393
+ debug.error('Token exchange failed:', error instanceof Error ? error.message : String(error));
424
394
  throw error;
425
395
  }
426
396
  }
@@ -4,6 +4,7 @@
4
4
  import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils';
5
5
  import type { LanguageMetadata } from '../utils/languageUtils';
6
6
  import type { OxyServicesBase } from '../OxyServices.base';
7
+ import { isDev } from '../shared/utils/debugUtils';
7
8
 
8
9
  export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base: T) {
9
10
  return class extends Base {
@@ -22,8 +23,10 @@ export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base:
22
23
 
23
24
  if (isReactNative) {
24
25
  try {
25
- const asyncStorageModule = await import('@react-native-async-storage/async-storage');
26
- const storage = (asyncStorageModule.default as unknown) as import('@react-native-async-storage/async-storage').AsyncStorageStatic;
26
+ // Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
27
+ const moduleName = '@react-native-async-storage/async-storage';
28
+ const asyncStorageModule = await import(moduleName);
29
+ const storage = asyncStorageModule.default as unknown as { getItem: (key: string) => Promise<string | null>; setItem: (key: string, value: string) => Promise<void>; removeItem: (key: string) => Promise<void> };
27
30
  return {
28
31
  getItem: storage.getItem.bind(storage),
29
32
  setItem: storage.setItem.bind(storage),
@@ -84,7 +87,7 @@ export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base:
84
87
 
85
88
  return null;
86
89
  } catch (error) {
87
- if (__DEV__) {
90
+ if (isDev()) {
88
91
  console.warn('Failed to get current language:', error);
89
92
  }
90
93
  return null;
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { BlockedUser, RestrictedUser } from '../models/interfaces';
5
5
  import type { OxyServicesBase } from '../OxyServices.base';
6
+ import { isDev } from '../shared/utils/debugUtils';
6
7
 
7
8
  export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base: T) {
8
9
  return class extends Base {
@@ -35,7 +36,7 @@ export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base:
35
36
  });
36
37
  } catch (error) {
37
38
  // If there's an error, assume not in list to avoid breaking functionality
38
- if (__DEV__) {
39
+ if (isDev()) {
39
40
  console.warn('Error checking user list:', error);
40
41
  }
41
42
  return false;
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { OxyServicesBase } from '../OxyServices.base';
5
5
  import type { SecurityActivity, SecurityActivityResponse, SecurityEventType } from '../models/interfaces';
6
+ import { isDev } from '../shared/utils/debugUtils';
6
7
 
7
8
  export function OxyServicesSecurityMixin<T extends typeof OxyServicesBase>(Base: T) {
8
9
  return class extends Base {
@@ -71,7 +72,7 @@ export function OxyServicesSecurityMixin<T extends typeof OxyServicesBase>(Base:
71
72
  } catch (error) {
72
73
  // Don't throw - logging failures shouldn't break user flow
73
74
  // But log for monitoring
74
- if (__DEV__) {
75
+ if (isDev()) {
75
76
  console.warn('[OxyServices] Failed to log private key exported event:', error);
76
77
  }
77
78
  }
@@ -93,7 +94,7 @@ export function OxyServicesSecurityMixin<T extends typeof OxyServicesBase>(Base:
93
94
  } catch (error) {
94
95
  // Don't throw - logging failures shouldn't break user flow
95
96
  // But log for monitoring
96
- if (__DEV__) {
97
+ if (isDev()) {
97
98
  console.warn('[OxyServices] Failed to log backup created event:', error);
98
99
  }
99
100
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tests for isDev() utility.
3
+ *
4
+ * These tests manipulate the global __DEV__ and process.env.NODE_ENV
5
+ * to verify isDev() works across RN, Node, and browser-like environments.
6
+ */
7
+
8
+ describe('isDev', () => {
9
+ const originalDev = (globalThis as any).__DEV__;
10
+ const originalNodeEnv = process.env.NODE_ENV;
11
+
12
+ afterEach(() => {
13
+ // Restore globals
14
+ if (originalDev === undefined) {
15
+ delete (globalThis as any).__DEV__;
16
+ } else {
17
+ (globalThis as any).__DEV__ = originalDev;
18
+ }
19
+ process.env.NODE_ENV = originalNodeEnv;
20
+
21
+ // Clear module cache so isDev re-evaluates
22
+ jest.resetModules();
23
+ });
24
+
25
+ async function loadIsDev() {
26
+ const mod = await import('../debugUtils');
27
+ return mod.isDev;
28
+ }
29
+
30
+ it('returns true when __DEV__ is true (React Native)', async () => {
31
+ (globalThis as any).__DEV__ = true;
32
+ const isDev = await loadIsDev();
33
+ expect(isDev()).toBe(true);
34
+ });
35
+
36
+ it('returns false when __DEV__ is false', async () => {
37
+ (globalThis as any).__DEV__ = false;
38
+ const isDev = await loadIsDev();
39
+ expect(isDev()).toBe(false);
40
+ });
41
+
42
+ it('falls back to NODE_ENV when __DEV__ is undefined', async () => {
43
+ delete (globalThis as any).__DEV__;
44
+ process.env.NODE_ENV = 'development';
45
+ const isDev = await loadIsDev();
46
+ expect(isDev()).toBe(true);
47
+ });
48
+
49
+ it('returns false when NODE_ENV is production and __DEV__ is undefined', async () => {
50
+ delete (globalThis as any).__DEV__;
51
+ process.env.NODE_ENV = 'production';
52
+ const isDev = await loadIsDev();
53
+ expect(isDev()).toBe(false);
54
+ });
55
+ });
@@ -14,7 +14,12 @@ declare const __DEV__: boolean | undefined;
14
14
  * Check if running in development mode
15
15
  */
16
16
  export const isDev = (): boolean => {
17
- return typeof __DEV__ !== 'undefined' && __DEV__;
17
+ if (typeof __DEV__ !== 'undefined') return __DEV__;
18
+ try {
19
+ return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
20
+ } catch {
21
+ return false;
22
+ }
18
23
  };
19
24
 
20
25
  /**
@@ -42,8 +42,10 @@ export class DeviceManager {
42
42
  }> {
43
43
  if (this.isReactNative()) {
44
44
  try {
45
- const asyncStorageModule = await import('@react-native-async-storage/async-storage');
46
- const storage = (asyncStorageModule.default as unknown) as import('@react-native-async-storage/async-storage').AsyncStorageStatic;
45
+ // Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
46
+ const moduleName = '@react-native-async-storage/async-storage';
47
+ const asyncStorageModule = await import(moduleName);
48
+ const storage = asyncStorageModule.default as unknown as { getItem: (key: string) => Promise<string | null>; setItem: (key: string, value: string) => Promise<void>; removeItem: (key: string) => Promise<void> };
47
49
  return {
48
50
  getItem: storage.getItem.bind(storage),
49
51
  setItem: storage.setItem.bind(storage),
@@ -108,8 +108,9 @@ export async function initPlatformFromReactNative(): Promise<void> {
108
108
  }
109
109
 
110
110
  try {
111
- // Dynamic import to avoid bundler issues
112
- const { Platform } = await import('react-native');
111
+ // Variable indirection prevents bundlers (Vite, webpack) from statically resolving this
112
+ const moduleName = 'react-native';
113
+ const { Platform } = await import(moduleName);
113
114
  setPlatformOS(Platform.OS as PlatformOS);
114
115
  } catch {
115
116
  // react-native not available, use detected platform