@oxyhq/core 1.11.12 → 1.11.14

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 (130) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/CrossDomainAuth.js +3 -1
  3. package/dist/cjs/HttpService.js +214 -33
  4. package/dist/cjs/OxyServices.base.js +9 -0
  5. package/dist/cjs/OxyServices.js +8 -3
  6. package/dist/cjs/crypto/index.js +3 -1
  7. package/dist/cjs/crypto/keyManager.js +476 -172
  8. package/dist/cjs/crypto/polyfill.js +14 -65
  9. package/dist/cjs/crypto/recoveryPhrase.js +30 -11
  10. package/dist/cjs/crypto/signatureService.js +25 -60
  11. package/dist/cjs/i18n/locales/en-US.json +46 -1
  12. package/dist/cjs/i18n/locales/es-ES.json +46 -1
  13. package/dist/cjs/i18n/locales/locales/en-US.json +46 -1
  14. package/dist/cjs/i18n/locales/locales/es-ES.json +46 -1
  15. package/dist/cjs/index.js +10 -2
  16. package/dist/cjs/mixins/OxyServices.assets.js +9 -4
  17. package/dist/cjs/mixins/OxyServices.auth.js +147 -14
  18. package/dist/cjs/mixins/OxyServices.contacts.js +50 -0
  19. package/dist/cjs/mixins/OxyServices.features.js +0 -11
  20. package/dist/cjs/mixins/OxyServices.fedcm.js +4 -3
  21. package/dist/cjs/mixins/OxyServices.language.js +5 -36
  22. package/dist/cjs/mixins/OxyServices.redirect.js +6 -2
  23. package/dist/cjs/mixins/OxyServices.security.js +13 -2
  24. package/dist/cjs/mixins/OxyServices.user.js +59 -38
  25. package/dist/cjs/mixins/OxyServices.utility.js +416 -110
  26. package/dist/cjs/mixins/index.js +11 -3
  27. package/dist/cjs/utils/accountUtils.js +71 -2
  28. package/dist/cjs/utils/deviceManager.js +5 -36
  29. package/dist/cjs/utils/languageUtils.js +22 -0
  30. package/dist/cjs/utils/platformCrypto.js +165 -0
  31. package/dist/cjs/utils/platformCrypto.native.js +123 -0
  32. package/dist/esm/.tsbuildinfo +1 -1
  33. package/dist/esm/CrossDomainAuth.js +3 -1
  34. package/dist/esm/HttpService.js +215 -34
  35. package/dist/esm/OxyServices.base.js +9 -0
  36. package/dist/esm/OxyServices.js +8 -3
  37. package/dist/esm/crypto/index.js +1 -1
  38. package/dist/esm/crypto/keyManager.js +473 -138
  39. package/dist/esm/crypto/polyfill.js +14 -32
  40. package/dist/esm/crypto/recoveryPhrase.js +30 -11
  41. package/dist/esm/crypto/signatureService.js +25 -27
  42. package/dist/esm/i18n/locales/en-US.json +46 -1
  43. package/dist/esm/i18n/locales/es-ES.json +46 -1
  44. package/dist/esm/i18n/locales/locales/en-US.json +46 -1
  45. package/dist/esm/i18n/locales/locales/es-ES.json +46 -1
  46. package/dist/esm/index.js +4 -3
  47. package/dist/esm/mixins/OxyServices.assets.js +9 -4
  48. package/dist/esm/mixins/OxyServices.auth.js +145 -14
  49. package/dist/esm/mixins/OxyServices.contacts.js +47 -0
  50. package/dist/esm/mixins/OxyServices.features.js +0 -11
  51. package/dist/esm/mixins/OxyServices.fedcm.js +4 -3
  52. package/dist/esm/mixins/OxyServices.language.js +5 -3
  53. package/dist/esm/mixins/OxyServices.redirect.js +6 -2
  54. package/dist/esm/mixins/OxyServices.security.js +13 -2
  55. package/dist/esm/mixins/OxyServices.user.js +59 -38
  56. package/dist/esm/mixins/OxyServices.utility.js +416 -77
  57. package/dist/esm/mixins/index.js +11 -3
  58. package/dist/esm/utils/accountUtils.js +67 -1
  59. package/dist/esm/utils/deviceManager.js +5 -3
  60. package/dist/esm/utils/languageUtils.js +21 -0
  61. package/dist/esm/utils/platformCrypto.js +125 -0
  62. package/dist/esm/utils/platformCrypto.native.js +80 -0
  63. package/dist/types/.tsbuildinfo +1 -1
  64. package/dist/types/HttpService.d.ts +47 -3
  65. package/dist/types/OxyServices.base.d.ts +7 -0
  66. package/dist/types/OxyServices.d.ts +50 -7
  67. package/dist/types/crypto/index.d.ts +1 -1
  68. package/dist/types/crypto/keyManager.d.ts +110 -9
  69. package/dist/types/crypto/polyfill.d.ts +3 -1
  70. package/dist/types/crypto/recoveryPhrase.d.ts +31 -7
  71. package/dist/types/crypto/signatureService.d.ts +4 -0
  72. package/dist/types/index.d.ts +7 -5
  73. package/dist/types/mixins/OxyServices.analytics.d.ts +1 -0
  74. package/dist/types/mixins/OxyServices.assets.d.ts +6 -10
  75. package/dist/types/mixins/OxyServices.auth.d.ts +82 -5
  76. package/dist/types/mixins/OxyServices.contacts.d.ts +99 -0
  77. package/dist/types/mixins/OxyServices.developer.d.ts +1 -0
  78. package/dist/types/mixins/OxyServices.devices.d.ts +1 -0
  79. package/dist/types/mixins/OxyServices.features.d.ts +2 -7
  80. package/dist/types/mixins/OxyServices.fedcm.d.ts +1 -0
  81. package/dist/types/mixins/OxyServices.karma.d.ts +1 -0
  82. package/dist/types/mixins/OxyServices.language.d.ts +1 -0
  83. package/dist/types/mixins/OxyServices.location.d.ts +1 -0
  84. package/dist/types/mixins/OxyServices.managedAccounts.d.ts +1 -0
  85. package/dist/types/mixins/OxyServices.payment.d.ts +1 -0
  86. package/dist/types/mixins/OxyServices.popup.d.ts +1 -0
  87. package/dist/types/mixins/OxyServices.privacy.d.ts +1 -0
  88. package/dist/types/mixins/OxyServices.redirect.d.ts +1 -0
  89. package/dist/types/mixins/OxyServices.security.d.ts +1 -0
  90. package/dist/types/mixins/OxyServices.topics.d.ts +1 -0
  91. package/dist/types/mixins/OxyServices.user.d.ts +28 -11
  92. package/dist/types/mixins/OxyServices.utility.d.ts +145 -10
  93. package/dist/types/mixins/index.d.ts +52 -4
  94. package/dist/types/models/interfaces.d.ts +62 -3
  95. package/dist/types/utils/accountUtils.d.ts +41 -1
  96. package/dist/types/utils/languageUtils.d.ts +1 -0
  97. package/dist/types/utils/platformCrypto.d.ts +87 -0
  98. package/dist/types/utils/platformCrypto.native.d.ts +54 -0
  99. package/package.json +45 -2
  100. package/src/CrossDomainAuth.ts +12 -10
  101. package/src/HttpService.ts +251 -40
  102. package/src/OxyServices.base.ts +10 -0
  103. package/src/OxyServices.ts +26 -7
  104. package/src/crypto/__tests__/keyManager.test.ts +336 -0
  105. package/src/crypto/index.ts +6 -1
  106. package/src/crypto/keyManager.ts +529 -151
  107. package/src/crypto/polyfill.ts +14 -34
  108. package/src/crypto/recoveryPhrase.ts +56 -17
  109. package/src/crypto/signatureService.ts +25 -30
  110. package/src/i18n/locales/en-US.json +46 -1
  111. package/src/i18n/locales/es-ES.json +46 -1
  112. package/src/index.ts +19 -4
  113. package/src/mixins/OxyServices.assets.ts +15 -11
  114. package/src/mixins/OxyServices.auth.ts +175 -15
  115. package/src/mixins/OxyServices.contacts.ts +73 -0
  116. package/src/mixins/OxyServices.features.ts +2 -12
  117. package/src/mixins/OxyServices.fedcm.ts +4 -3
  118. package/src/mixins/OxyServices.language.ts +6 -4
  119. package/src/mixins/OxyServices.redirect.ts +6 -2
  120. package/src/mixins/OxyServices.security.ts +18 -8
  121. package/src/mixins/OxyServices.user.ts +72 -49
  122. package/src/mixins/OxyServices.utility.ts +562 -89
  123. package/src/mixins/__tests__/serviceAuth.test.ts +623 -0
  124. package/src/mixins/index.ts +58 -7
  125. package/src/models/interfaces.ts +65 -3
  126. package/src/utils/accountUtils.ts +82 -2
  127. package/src/utils/deviceManager.ts +7 -4
  128. package/src/utils/languageUtils.ts +23 -2
  129. package/src/utils/platformCrypto.native.ts +101 -0
  130. package/src/utils/platformCrypto.ts +145 -0
@@ -25,9 +25,53 @@ import { OxyServicesUtilityMixin } from './OxyServices.utility';
25
25
  import { OxyServicesFeaturesMixin } from './OxyServices.features';
26
26
  import { OxyServicesTopicsMixin } from './OxyServices.topics';
27
27
  import { OxyServicesManagedAccountsMixin } from './OxyServices.managedAccounts';
28
+ import { OxyServicesContactsMixin } from './OxyServices.contacts';
28
29
 
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
- type MixinFunction = (Base: any) => any;
30
+ /**
31
+ * Instance shape of every mixin in the pipeline, intersected. The runtime
32
+ * `composeOxyServices()` produces a class whose instances expose all of
33
+ * these methods; we surface that to TypeScript via this intersection so the
34
+ * `extends` site in `OxyServices.ts` can avoid an `as any` cast.
35
+ *
36
+ * If you add a new mixin to `MIXIN_PIPELINE`, add it here too so its methods
37
+ * are visible without a cast.
38
+ */
39
+ type AllMixinInstances =
40
+ & InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>>
41
+ & InstanceType<ReturnType<typeof OxyServicesFedCMMixin<typeof OxyServicesBase>>>
42
+ & InstanceType<ReturnType<typeof OxyServicesPopupAuthMixin<typeof OxyServicesBase>>>
43
+ & InstanceType<ReturnType<typeof OxyServicesRedirectAuthMixin<typeof OxyServicesBase>>>
44
+ & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>>
45
+ & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>>
46
+ & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>>
47
+ & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>>
48
+ & InstanceType<ReturnType<typeof OxyServicesKarmaMixin<typeof OxyServicesBase>>>
49
+ & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>>
50
+ & InstanceType<ReturnType<typeof OxyServicesDeveloperMixin<typeof OxyServicesBase>>>
51
+ & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>>
52
+ & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>>
53
+ & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>>
54
+ & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>>
55
+ & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>>
56
+ & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>>
57
+ & InstanceType<ReturnType<typeof OxyServicesManagedAccountsMixin<typeof OxyServicesBase>>>
58
+ & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>>
59
+ & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
60
+
61
+ /**
62
+ * Constructor type for the fully composed mixin pipeline. Each mixin returns
63
+ * a new constructor that augments its input; reducing across the pipeline
64
+ * yields an instance with every mixin's methods.
65
+ */
66
+ export type ComposedOxyServicesConstructor = new (config: import('../OxyServices.base').OxyConfig) => AllMixinInstances;
67
+
68
+ /**
69
+ * A mixin function: takes a constructor and returns an augmented constructor.
70
+ * Each individual mixin uses a `<T extends typeof OxyServicesBase>` generic
71
+ * to preserve its specific augmentations, but those refinements are
72
+ * intentionally collapsed across the `reduce` call below.
73
+ */
74
+ type MixinFunction = (Base: new (...args: unknown[]) => OxyServicesBase) => new (...args: unknown[]) => OxyServicesBase;
31
75
 
32
76
  /**
33
77
  * Mixin pipeline - applied in order from first to last.
@@ -70,6 +114,7 @@ const MIXIN_PIPELINE: MixinFunction[] = [
70
114
  OxyServicesFeaturesMixin,
71
115
  OxyServicesTopicsMixin,
72
116
  OxyServicesManagedAccountsMixin,
117
+ OxyServicesContactsMixin,
73
118
 
74
119
  // Utility (last, can use all above)
75
120
  OxyServicesUtilityMixin,
@@ -79,15 +124,21 @@ const MIXIN_PIPELINE: MixinFunction[] = [
79
124
  * Composes all OxyServices mixins using a pipeline pattern.
80
125
  *
81
126
  * This is equivalent to the nested calls but more readable and maintainable.
82
- * Adding a new mixin: just add it to MIXIN_PIPELINE at the appropriate position.
127
+ * Adding a new mixin: add it to MIXIN_PIPELINE at the appropriate position
128
+ * AND extend `AllMixinInstances` so its methods are visible to consumers.
129
+ *
130
+ * The cast through `unknown` carries the runtime augmentation chain into the
131
+ * static type system. `Array.reduce` cannot track each mixin's generic
132
+ * refinement, so we assert the final shape exposed by all mixins together.
83
133
  *
84
- * @returns The fully composed OxyServices class with all mixins applied
134
+ * @returns The fully composed OxyServices constructor with all mixins applied
85
135
  */
86
- export function composeOxyServices() {
87
- return MIXIN_PIPELINE.reduce(
136
+ export function composeOxyServices(): ComposedOxyServicesConstructor {
137
+ const composed = MIXIN_PIPELINE.reduce(
88
138
  (Base, mixin) => mixin(Base),
89
- OxyServicesBase as unknown as ReturnType<MixinFunction>
139
+ OxyServicesBase as unknown as new (...args: unknown[]) => OxyServicesBase
90
140
  );
141
+ return composed as unknown as ComposedOxyServicesConstructor;
91
142
  }
92
143
 
93
144
  // Export the pipeline for testing/debugging
@@ -23,6 +23,41 @@ export interface OxyConfig {
23
23
  onRequestError?: (url: string, method: string, error: Error) => void;
24
24
  }
25
25
 
26
+ /**
27
+ * Privacy settings for a user account.
28
+ *
29
+ * All fields are optional because:
30
+ * - Updates are dot-path partial PATCHes — clients send only changed keys.
31
+ * - The server may return a partial subdocument depending on the API
32
+ * build (older builds returned only the field that changed).
33
+ * - User accounts created before a new toggle was introduced won't
34
+ * have that key persisted yet.
35
+ *
36
+ * Mirrors `IPrivacySettings` from `packages/api/src/types/privacy.types.ts`,
37
+ * but with every field marked optional.
38
+ */
39
+ export interface PrivacySettings {
40
+ isPrivateAccount?: boolean;
41
+ hideOnlineStatus?: boolean;
42
+ hideLastSeen?: boolean;
43
+ profileVisibility?: boolean;
44
+ loginAlerts?: boolean;
45
+ blockScreenshots?: boolean;
46
+ login?: boolean;
47
+ biometricLogin?: boolean;
48
+ showActivity?: boolean;
49
+ allowTagging?: boolean;
50
+ allowMentions?: boolean;
51
+ hideReadReceipts?: boolean;
52
+ allowDirectMessages?: boolean;
53
+ dataSharing?: boolean;
54
+ locationSharing?: boolean;
55
+ analyticsSharing?: boolean;
56
+ sensitiveContent?: boolean;
57
+ autoFilter?: boolean;
58
+ muteKeywords?: boolean;
59
+ }
60
+
26
61
  export interface User {
27
62
  id: string;
28
63
  publicKey: string;
@@ -33,9 +68,7 @@ export interface User {
33
68
  // Named color preset (e.g. 'teal', 'blue', 'purple')
34
69
  color?: string;
35
70
  // Privacy and security settings
36
- privacySettings?: {
37
- [key: string]: unknown;
38
- };
71
+ privacySettings?: PrivacySettings;
39
72
  name?: {
40
73
  first?: string;
41
74
  last?: string;
@@ -289,6 +322,35 @@ export interface FileDeleteResponse {
289
322
  fileId: string;
290
323
  }
291
324
 
325
+ /**
326
+ * React Native file descriptor accepted by FormData.
327
+ *
328
+ * On React Native, the multipart upload reads the file from disk via the URI
329
+ * during the network request — no in-JS Blob construction is required (and
330
+ * doing so would fail on Hermes since RN's BlobManager cannot wrap an
331
+ * ArrayBuffer/ArrayBufferView).
332
+ *
333
+ * This shape matches what `expo-document-picker` and `expo-image-picker`
334
+ * return for selected assets, and is what `OxyServices.assetUpload` accepts
335
+ * on native platforms.
336
+ */
337
+ export interface RNFileDescriptor {
338
+ uri: string;
339
+ type?: string;
340
+ name?: string;
341
+ size?: number;
342
+ }
343
+
344
+ /**
345
+ * Asset upload input — accepted by `OxyServices.assetUpload` and `uploadRawFile`.
346
+ *
347
+ * - `File` / `Blob`: standard web browser path. `assetUpload` appends the
348
+ * Blob to FormData directly.
349
+ * - {@link RNFileDescriptor}: React Native path. FormData reads the file from
350
+ * disk via the URI during the multipart request.
351
+ */
352
+ export type AssetUploadInput = File | Blob | RNFileDescriptor;
353
+
292
354
  /**
293
355
  * Central Asset Service interfaces
294
356
  */
@@ -3,6 +3,8 @@
3
3
  * Used by both @oxyhq/services (React Native) and @oxyhq/auth (Web) account stores.
4
4
  */
5
5
 
6
+ import { translate } from '../i18n';
7
+
6
8
  export interface QuickAccount {
7
9
  sessionId: string;
8
10
  userId?: string;
@@ -12,6 +14,83 @@ export interface QuickAccount {
12
14
  avatarUrl?: string;
13
15
  }
14
16
 
17
+ /** Minimal user shape accepted by display-name helpers. Avoids importing the full User type. */
18
+ export interface DisplayNameUserShape {
19
+ name?: string | { first?: string; last?: string; full?: string; [key: string]: unknown };
20
+ username?: string;
21
+ publicKey?: string;
22
+ }
23
+
24
+ /**
25
+ * Truncate a long public key for display, e.g. `0x12345678…`.
26
+ * Falls back to the raw key if it's too short to truncate.
27
+ */
28
+ export const formatPublicKeyHandle = (publicKey: string): string => {
29
+ const cleaned = publicKey.startsWith('0x') ? publicKey.slice(2) : publicKey;
30
+ if (cleaned.length <= 8) return `0x${cleaned}`;
31
+ return `0x${cleaned.slice(0, 8)}…`;
32
+ };
33
+
34
+ /**
35
+ * Resolve a friendly display name for a user.
36
+ *
37
+ * Order of preference:
38
+ * 1. `name.full`, or composed `name.first name.last`
39
+ * 2. `name` (when stored as a plain string)
40
+ * 3. `username`
41
+ * 4. `Account 0x12345678…` (derived from publicKey, when present)
42
+ * 5. Translated fallback (e.g. "Unnamed")
43
+ *
44
+ * The translation key `common.unnamed` is used for the final fallback. If the
45
+ * caller does not pass a locale, the default English translation is used.
46
+ */
47
+ export const getAccountDisplayName = (
48
+ user: DisplayNameUserShape | null | undefined,
49
+ locale?: string,
50
+ ): string => {
51
+ if (!user) return translate(locale, 'common.unnamed');
52
+
53
+ const { name, username, publicKey } = user;
54
+
55
+ if (name && typeof name === 'object') {
56
+ if (typeof name.full === 'string' && name.full.trim()) return name.full.trim();
57
+ const first = typeof name.first === 'string' ? name.first.trim() : '';
58
+ const last = typeof name.last === 'string' ? name.last.trim() : '';
59
+ const composed = [first, last].filter(Boolean).join(' ').trim();
60
+ if (composed) return composed;
61
+ } else if (typeof name === 'string' && name.trim()) {
62
+ return name.trim();
63
+ }
64
+
65
+ if (typeof username === 'string' && username.trim()) return username.trim();
66
+
67
+ if (typeof publicKey === 'string' && publicKey.length > 0) {
68
+ return translate(locale, 'common.accountFallback', {
69
+ handle: formatPublicKeyHandle(publicKey),
70
+ });
71
+ }
72
+
73
+ return translate(locale, 'common.unnamed');
74
+ };
75
+
76
+ /**
77
+ * Resolve a `@handle` style identifier for a user.
78
+ *
79
+ * Returns the bare username when present (without the `@`), otherwise a
80
+ * truncated public-key handle (`0x12345678…`), or `undefined` when neither is
81
+ * available — callers can decide whether to hide the line entirely.
82
+ */
83
+ export const getAccountFallbackHandle = (
84
+ user: DisplayNameUserShape | null | undefined,
85
+ ): string | undefined => {
86
+ if (!user) return undefined;
87
+ if (typeof user.username === 'string' && user.username.trim()) return user.username.trim();
88
+ if (typeof user.publicKey === 'string' && user.publicKey.length > 0) {
89
+ return formatPublicKeyHandle(user.publicKey);
90
+ }
91
+ return undefined;
92
+ };
93
+
15
94
  /**
16
95
  * Build an ordered array of QuickAccounts from a map and order list.
17
96
  */
@@ -38,8 +117,9 @@ export const buildAccountsArray = (
38
117
  export const createQuickAccount = (
39
118
  sessionId: string,
40
119
  userData: {
41
- name?: { full?: string; first?: string };
120
+ name?: string | { full?: string; first?: string; last?: string };
42
121
  username?: string;
122
+ publicKey?: string;
43
123
  id?: string;
44
124
  _id?: { toString(): string } | string;
45
125
  avatar?: string;
@@ -47,7 +127,7 @@ export const createQuickAccount = (
47
127
  existingAccount?: QuickAccount,
48
128
  getFileDownloadUrl?: (fileId: string, variant: string) => string
49
129
  ): QuickAccount => {
50
- const displayName = userData.name?.full || userData.name?.first || userData.username || 'Account';
130
+ const displayName = getAccountDisplayName(userData);
51
131
  const userId = userData.id || (typeof userData._id === 'string' ? userData._id : userData._id?.toString());
52
132
 
53
133
  // Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
@@ -1,3 +1,5 @@
1
+ import { loadAsyncStorage } from './platformCrypto';
2
+
1
3
  export interface DeviceFingerprint {
2
4
  userAgent: string;
3
5
  platform: string;
@@ -42,10 +44,11 @@ export class DeviceManager {
42
44
  }> {
43
45
  if (this.isReactNative()) {
44
46
  try {
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(/* @vite-ignore */ 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
+ // `loadAsyncStorage` is per-platform: the RN variant statically imports
48
+ // @react-native-async-storage/async-storage, the default variant throws
49
+ // (never called outside RN because of the `isReactNative()` gate above).
50
+ const asyncStorageModule = await loadAsyncStorage();
51
+ const storage = asyncStorageModule.default;
49
52
  return {
50
53
  getItem: storage.getItem.bind(storage),
51
54
  setItem: storage.setItem.bind(storage),
@@ -154,7 +154,7 @@ export function getNativeLanguageName(languageCode: string | null | undefined):
154
154
  export function normalizeLanguageCode(lang?: string | null): string {
155
155
  if (!lang) return FALLBACK_LANGUAGE;
156
156
  if (lang.includes('-')) return lang;
157
-
157
+
158
158
  const map: Record<string, string> = {
159
159
  en: 'en-US',
160
160
  es: 'es-ES',
@@ -168,7 +168,28 @@ export function normalizeLanguageCode(lang?: string | null): string {
168
168
  zh: 'zh-CN',
169
169
  ar: 'ar-SA',
170
170
  };
171
-
171
+
172
172
  return map[lang] || lang;
173
173
  }
174
174
 
175
+ /**
176
+ * RTL language detection.
177
+ *
178
+ * Returns `true` when the given BCP-47 tag or bare language code is one
179
+ * of the right-to-left scripts we ship UI for. Apps use this to drive
180
+ * `I18nManager.allowRTL(true)` / `forceRTL(...)` on React Native and the
181
+ * `<html dir="rtl">` attribute on web.
182
+ *
183
+ * Includes Arabic (`ar`), Hebrew (`he` / legacy `iw`), Persian (`fa`),
184
+ * Urdu (`ur`) plus their common region variants. Unknown tags are
185
+ * treated as LTR.
186
+ */
187
+ const RTL_LANGUAGE_BASES = new Set(['ar', 'he', 'iw', 'fa', 'ur']);
188
+
189
+ export function isRTLLocale(locale?: string | null): boolean {
190
+ if (!locale) return false;
191
+ const base = locale.toLowerCase().split('-')[0];
192
+ if (!base) return false;
193
+ return RTL_LANGUAGE_BASES.has(base);
194
+ }
195
+
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Platform Crypto / Storage — React Native Variant
3
+ *
4
+ * Companion to `./platformCrypto.ts`. See the doc-comment at the top of that
5
+ * file for the full design.
6
+ *
7
+ * Metro auto-selects this file in any non-web build (`preferNativePlatform`
8
+ * is `true` for iOS / Android, so `*.native.js` shadows `*.js` during
9
+ * source-extension resolution inside `node_modules/@oxyhq/core/dist/`). On
10
+ * iOS / Android `<base>.ios.js` / `<base>.android.js` would shadow this file
11
+ * if they existed, but they don't — `.native.js` is the shared RN variant.
12
+ *
13
+ * - The default variant references Node's `'crypto'` and would crash Metro
14
+ * if bundled into an RN app.
15
+ * - This variant references the RN-only modules (`expo-crypto`,
16
+ * `expo-secure-store`, `@react-native-async-storage/async-storage`)
17
+ * as static imports, so Metro and Hermes both resolve and parse them
18
+ * cleanly.
19
+ *
20
+ * Both variants expose the same surface; importers don't care which one
21
+ * they got.
22
+ *
23
+ * # Why static imports?
24
+ *
25
+ * Every RN consumer of `@oxyhq/core` already lists or transitively pulls
26
+ * in `expo-crypto`, `expo-secure-store`, and
27
+ * `@react-native-async-storage/async-storage` (they're stable Expo modules
28
+ * present in `services`, `accounts`, `inbox`, and `test-app`). A static
29
+ * import is what Metro wants to see anyway, and Hermes parses it like any
30
+ * other ES module — no `Function`-constructor parser exotic-mode involved.
31
+ *
32
+ * This is also clearer to debug: Metro fails up-front with a normal
33
+ * unresolved-module error if a consumer is missing a peer dep, instead of
34
+ * a confusing runtime throw the first time a code path that needs the
35
+ * module is exercised.
36
+ */
37
+
38
+ import * as ExpoCrypto from 'expo-crypto';
39
+ import * as SecureStore from 'expo-secure-store';
40
+ import AsyncStorage from '@react-native-async-storage/async-storage';
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Node `crypto` — never available in RN.
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export async function loadNodeCrypto(): Promise<typeof import('crypto')> {
47
+ // Unreachable in practice: every caller gates with `isNodeJS()` before
48
+ // invoking this. If it somehow does fire, throw immediately with a clear
49
+ // diagnostic rather than letting Metro / Hermes attempt to find a
50
+ // non-existent module at runtime.
51
+ throw new Error(
52
+ "[oxy.platformCrypto] Node's built-in 'crypto' module is not available " +
53
+ 'in a React Native runtime. Use the RN-specific helpers ' +
54
+ '(loadExpoCrypto, getRandomBytesRN) or the Web Crypto API (`globalThis.crypto`).',
55
+ );
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // expo-crypto — RN cryptographic primitives.
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export async function loadExpoCrypto(): Promise<typeof import('expo-crypto')> {
63
+ return ExpoCrypto;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // expo-secure-store — RN keychain / keystore.
68
+ // ---------------------------------------------------------------------------
69
+
70
+ export async function loadSecureStore(): Promise<typeof import('expo-secure-store')> {
71
+ return SecureStore;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // @react-native-async-storage/async-storage — RN persistent KV storage.
76
+ // ---------------------------------------------------------------------------
77
+
78
+ type AsyncStorageLike = {
79
+ getItem: (key: string) => Promise<string | null>;
80
+ setItem: (key: string, value: string) => Promise<void>;
81
+ removeItem: (key: string) => Promise<void>;
82
+ };
83
+
84
+ export async function loadAsyncStorage(): Promise<{ default: AsyncStorageLike }> {
85
+ // Mirror the shape callers historically used (`module.default.<method>`)
86
+ // so the call sites don't have to know whether the underlying module
87
+ // ships ESM or CJS-with-default.
88
+ const storage = AsyncStorage as unknown as AsyncStorageLike;
89
+ return {
90
+ default: storage,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Synchronous random-bytes via `expo-crypto.getRandomBytes`. Available
96
+ * synchronously because `expo-crypto` is statically imported by this file
97
+ * — no async initialization race.
98
+ */
99
+ export function getRandomBytesRN(byteCount: number): Uint8Array {
100
+ return ExpoCrypto.getRandomBytes(byteCount);
101
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Platform Crypto / Storage — Default Variant (Node.js, Browser, generic bundlers)
3
+ *
4
+ * Provides lazy access to platform-specific crypto and storage modules.
5
+ *
6
+ * # Variants
7
+ *
8
+ * This module ships in two physical variants on disk, selected per consumer
9
+ * by the bundler / runtime:
10
+ *
11
+ * - `platformCrypto.js` — this file. Used by Node.js, Vite, webpack,
12
+ * Rollup, esbuild, and anything that does
13
+ * not match Metro's `*.native.js`
14
+ * source-extension preference.
15
+ * - `platformCrypto.native.js` — sibling file. Picked up automatically by
16
+ * Metro's resolver (which prefers
17
+ * `*.<platform>.js` and `*.native.js` over
18
+ * plain `*.js` when `preferNativePlatform`
19
+ * is true — Expo sets this for all non-web
20
+ * builds).
21
+ *
22
+ * The `package.json#exports` map also declares a `"react-native"` condition
23
+ * pointing at the same `dist/esm/index.js` entry — that entry transitively
24
+ * imports `./platformCrypto`, and Metro's per-file source-extension lookup
25
+ * substitutes the `.native.js` sibling automatically inside `dist/`. This
26
+ * means consumers never have to add resolver shims; Metro Just Works.
27
+ *
28
+ * Both variants expose the EXACT same public API; importers don't need to know
29
+ * which one they got. The variant difference is purely about which underlying
30
+ * native modules each one references:
31
+ *
32
+ * ┌──────────────────┬───────────────────────┬───────────────────────────────┐
33
+ * │ Function │ Default variant │ React Native variant │
34
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
35
+ * │ loadNodeCrypto │ `await import('crypto')` (Node built-in) │
36
+ * │ │ │ throws — Node crypto is not │
37
+ * │ │ │ available on Hermes/RN │
38
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
39
+ * │ loadExpoCrypto │ throws — expo-crypto │ static `import 'expo-crypto'` │
40
+ * │ │ is not part of a │ │
41
+ * │ │ Node/Vite bundle │ │
42
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
43
+ * │ loadSecureStore │ throws (web/Node have │ static `import 'expo-secure-` │
44
+ * │ │ their own storage) │ store' │
45
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
46
+ * │ loadAsyncStorage │ throws (web/Node have │ static `import '@react- │
47
+ * │ │ their own storage) │ native-async-storage/...' │
48
+ * ├──────────────────┼───────────────────────┼───────────────────────────────┤
49
+ * │ getRandomBytesRN │ throws (RN-only) │ direct call into expo-crypto │
50
+ * └──────────────────┴───────────────────────┴───────────────────────────────┘
51
+ *
52
+ * Crucially, the default variant references ONLY Node's `'crypto'`. It never
53
+ * mentions `expo-*` or `@react-native-async-storage/*` — so Vite, webpack,
54
+ * esbuild, Rollup, and Node itself can bundle / require it without ever
55
+ * attempting to resolve those RN-only packages.
56
+ *
57
+ * The React Native variant references ONLY the RN packages. It never
58
+ * mentions `'crypto'` — so Metro and Hermes have nothing to choke on.
59
+ *
60
+ * # Why not a single file with dynamic import?
61
+ *
62
+ * A previous iteration used a "bundler-opaque" `new Function('s', 'return
63
+ * import(s)')` trick so a single file could service every platform. It
64
+ * bundled cleanly on Metro but Hermes refused to PARSE the resulting
65
+ * `import()` expression inside a Function-constructor body
66
+ * (`SyntaxError: Invalid expression encountered` at the `(` of `import(`).
67
+ * The platform-extension split is the only approach that lets each runtime
68
+ * see a file containing only specifiers it can understand — no tricks, no
69
+ * runtime parsing risks.
70
+ */
71
+
72
+ import { isReactNative } from './platform';
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Node `crypto` — Node built-in
76
+ //
77
+ // `await import('crypto')` here is a real, static-from-tsc's-perspective
78
+ // dynamic import. Node ESM, Vite, webpack, and esbuild all resolve it fine.
79
+ // Metro never sees this file because the `.react-native.js` sibling shadows
80
+ // it, so Metro never tries to resolve `'crypto'`.
81
+ // ---------------------------------------------------------------------------
82
+
83
+ let cachedNodeCrypto: typeof import('crypto') | null = null;
84
+
85
+ export async function loadNodeCrypto(): Promise<typeof import('crypto')> {
86
+ if (cachedNodeCrypto) {
87
+ return cachedNodeCrypto;
88
+ }
89
+ cachedNodeCrypto = await import('node:crypto');
90
+ return cachedNodeCrypto;
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // RN-only modules — never called from this variant.
95
+ //
96
+ // These throw a clear error if anything ever reaches them outside RN. In
97
+ // practice every caller gates with `isReactNative()` before calling, so
98
+ // these are belt-and-braces.
99
+ // ---------------------------------------------------------------------------
100
+
101
+ function notReactNativeError(module: string): Error {
102
+ return new Error(
103
+ `[oxy.platformCrypto] Tried to load '${module}' outside React Native. This module is only available in a React Native runtime; bundling routed this consumer to the default (Node/web) variant. This indicates a missing platform gate (\`isReactNative()\`) in the calling code.`,
104
+ );
105
+ }
106
+
107
+ export async function loadExpoCrypto(): Promise<typeof import('expo-crypto')> {
108
+ if (isReactNative()) {
109
+ // Should be unreachable: when running on RN, Metro / the `react-native`
110
+ // exports condition serves the sibling variant. If we got here, the
111
+ // package-exports map is misconfigured for this host. Throw with a
112
+ // helpful diagnostic rather than fall back to a broken dynamic import.
113
+ throw new Error(
114
+ '[oxy.platformCrypto] React Native runtime resolved the default ' +
115
+ '(non-RN) variant of @oxyhq/core/utils/platformCrypto. Check the ' +
116
+ "consumer's bundler resolution — Metro should pick the sibling " +
117
+ '.react-native.js file via package exports.',
118
+ );
119
+ }
120
+ throw notReactNativeError('expo-crypto');
121
+ }
122
+
123
+ export async function loadSecureStore(): Promise<typeof import('expo-secure-store')> {
124
+ throw notReactNativeError('expo-secure-store');
125
+ }
126
+
127
+ export async function loadAsyncStorage(): Promise<{
128
+ default: {
129
+ getItem: (key: string) => Promise<string | null>;
130
+ setItem: (key: string, value: string) => Promise<void>;
131
+ removeItem: (key: string) => Promise<void>;
132
+ };
133
+ }> {
134
+ throw notReactNativeError('@react-native-async-storage/async-storage');
135
+ }
136
+
137
+ /**
138
+ * Synchronous random-bytes via `expo-crypto.getRandomBytes`. Only available
139
+ * in the React Native variant. The default variant throws because Node and
140
+ * browsers have their own native CSPRNGs (`crypto.randomBytes` and
141
+ * `crypto.getRandomValues` respectively) — callers should use those.
142
+ */
143
+ export function getRandomBytesRN(_byteCount: number): Uint8Array {
144
+ throw notReactNativeError('expo-crypto.getRandomBytes (sync)');
145
+ }