@webority-technologies/mobile-core 0.0.1 → 0.0.2

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 (146) hide show
  1. package/README.md +22 -1
  2. package/lib/commonjs/api/baseUrl.js +34 -0
  3. package/lib/commonjs/api/curl.js +109 -0
  4. package/lib/commonjs/api/publicClient.js +11 -20
  5. package/lib/commonjs/api/retry.js +75 -0
  6. package/lib/commonjs/api/userClient.js +46 -22
  7. package/lib/commonjs/auth/authStore.js +101 -33
  8. package/lib/commonjs/auth/jwt.js +32 -8
  9. package/lib/commonjs/config/index.js +2 -1
  10. package/lib/commonjs/deepLink/index.js +286 -0
  11. package/lib/commonjs/download/adapters/expoFs.js +100 -0
  12. package/lib/commonjs/download/adapters/shared.js +13 -0
  13. package/lib/commonjs/download/index.js +272 -0
  14. package/lib/commonjs/formatters/date.js +1 -1
  15. package/lib/commonjs/formatters/index.js +6 -0
  16. package/lib/commonjs/formatters/phone.js +103 -15
  17. package/lib/commonjs/geolocation/adapters/expoLocation.js +126 -0
  18. package/lib/commonjs/geolocation/index.js +339 -0
  19. package/lib/commonjs/index.js +133 -48
  20. package/lib/commonjs/initSDK.js +23 -2
  21. package/lib/commonjs/initUserAuth.js +2 -1
  22. package/lib/commonjs/logger/index.js +159 -5
  23. package/lib/commonjs/network/index.js +6 -0
  24. package/lib/commonjs/network/networkStatus.js +50 -15
  25. package/lib/commonjs/permissions/index.js +24 -1
  26. package/lib/commonjs/sqlite/adapters/expo.js +53 -0
  27. package/lib/commonjs/sqlite/adapters/op.js +43 -0
  28. package/lib/commonjs/sqlite/adapters/rows.js +33 -0
  29. package/lib/commonjs/sqlite/adapters/storage.js +50 -0
  30. package/lib/commonjs/sqlite/adapters/sync.js +45 -0
  31. package/lib/commonjs/sqlite/index.js +610 -0
  32. package/lib/commonjs/sqlite/nitro.js +17 -0
  33. package/lib/commonjs/sqlite/op.js +18 -0
  34. package/lib/commonjs/sqlite/quick.js +14 -0
  35. package/lib/commonjs/sqlite/storage.js +14 -0
  36. package/lib/commonjs/storage/index.js +40 -19
  37. package/lib/commonjs/types/globals.d.js +6 -0
  38. package/lib/commonjs/utils/imageCompression.js +51 -14
  39. package/lib/commonjs/utils/index.js +6 -0
  40. package/lib/commonjs/versionCheck/index.js +28 -4
  41. package/lib/module/api/baseUrl.js +30 -0
  42. package/lib/module/api/curl.js +104 -0
  43. package/lib/module/api/publicClient.js +11 -20
  44. package/lib/module/api/retry.js +69 -0
  45. package/lib/module/api/userClient.js +45 -21
  46. package/lib/module/auth/authStore.js +99 -32
  47. package/lib/module/auth/jwt.js +32 -8
  48. package/lib/module/config/index.js +2 -1
  49. package/lib/module/deepLink/index.js +280 -0
  50. package/lib/module/download/adapters/expoFs.js +95 -0
  51. package/lib/module/download/adapters/shared.js +8 -0
  52. package/lib/module/download/index.js +264 -0
  53. package/lib/module/formatters/date.js +1 -1
  54. package/lib/module/formatters/index.js +1 -1
  55. package/lib/module/formatters/phone.js +99 -13
  56. package/lib/module/geolocation/adapters/expoLocation.js +121 -0
  57. package/lib/module/geolocation/index.js +332 -0
  58. package/lib/module/index.js +28 -11
  59. package/lib/module/initSDK.js +23 -2
  60. package/lib/module/initUserAuth.js +2 -1
  61. package/lib/module/logger/index.js +156 -4
  62. package/lib/module/network/index.js +1 -1
  63. package/lib/module/network/networkStatus.js +47 -14
  64. package/lib/module/permissions/index.js +22 -0
  65. package/lib/module/sqlite/adapters/expo.js +48 -0
  66. package/lib/module/sqlite/adapters/op.js +38 -0
  67. package/lib/module/sqlite/adapters/rows.js +28 -0
  68. package/lib/module/sqlite/adapters/storage.js +46 -0
  69. package/lib/module/sqlite/adapters/sync.js +41 -0
  70. package/lib/module/sqlite/index.js +600 -0
  71. package/lib/module/sqlite/nitro.js +12 -0
  72. package/lib/module/sqlite/op.js +13 -0
  73. package/lib/module/sqlite/quick.js +9 -0
  74. package/lib/module/sqlite/storage.js +9 -0
  75. package/lib/module/storage/index.js +37 -18
  76. package/lib/module/types/globals.d.js +10 -0
  77. package/lib/module/utils/imageCompression.js +49 -13
  78. package/lib/module/utils/index.js +1 -1
  79. package/lib/module/versionCheck/index.js +27 -4
  80. package/lib/typescript/commonjs/api/baseUrl.d.ts +20 -0
  81. package/lib/typescript/commonjs/api/curl.d.ts +18 -0
  82. package/lib/typescript/commonjs/api/retry.d.ts +27 -0
  83. package/lib/typescript/commonjs/api/userClient.d.ts +11 -0
  84. package/lib/typescript/commonjs/auth/authStore.d.ts +29 -6
  85. package/lib/typescript/commonjs/config/index.d.ts +6 -0
  86. package/lib/typescript/commonjs/deepLink/index.d.ts +54 -0
  87. package/lib/typescript/commonjs/download/adapters/expoFs.d.ts +45 -0
  88. package/lib/typescript/commonjs/download/adapters/shared.d.ts +3 -0
  89. package/lib/typescript/commonjs/download/index.d.ts +76 -0
  90. package/lib/typescript/commonjs/formatters/index.d.ts +2 -1
  91. package/lib/typescript/commonjs/formatters/phone.d.ts +16 -1
  92. package/lib/typescript/commonjs/geolocation/adapters/expoLocation.d.ts +42 -0
  93. package/lib/typescript/commonjs/geolocation/index.d.ts +89 -0
  94. package/lib/typescript/commonjs/index.d.ts +30 -13
  95. package/lib/typescript/commonjs/initSDK.d.ts +32 -0
  96. package/lib/typescript/commonjs/logger/index.d.ts +39 -0
  97. package/lib/typescript/commonjs/network/index.d.ts +2 -2
  98. package/lib/typescript/commonjs/network/networkStatus.d.ts +16 -0
  99. package/lib/typescript/commonjs/permissions/index.d.ts +35 -0
  100. package/lib/typescript/commonjs/sqlite/adapters/expo.d.ts +30 -0
  101. package/lib/typescript/commonjs/sqlite/adapters/op.d.ts +27 -0
  102. package/lib/typescript/commonjs/sqlite/adapters/rows.d.ts +7 -0
  103. package/lib/typescript/commonjs/sqlite/adapters/storage.d.ts +30 -0
  104. package/lib/typescript/commonjs/sqlite/adapters/sync.d.ts +31 -0
  105. package/lib/typescript/commonjs/sqlite/index.d.ts +121 -0
  106. package/lib/typescript/commonjs/sqlite/nitro.d.ts +7 -0
  107. package/lib/typescript/commonjs/sqlite/op.d.ts +8 -0
  108. package/lib/typescript/commonjs/sqlite/quick.d.ts +4 -0
  109. package/lib/typescript/commonjs/sqlite/storage.d.ts +4 -0
  110. package/lib/typescript/commonjs/storage/index.d.ts +24 -2
  111. package/lib/typescript/commonjs/utils/imageCompression.d.ts +17 -0
  112. package/lib/typescript/commonjs/utils/index.d.ts +2 -2
  113. package/lib/typescript/module/api/baseUrl.d.ts +20 -0
  114. package/lib/typescript/module/api/curl.d.ts +18 -0
  115. package/lib/typescript/module/api/retry.d.ts +27 -0
  116. package/lib/typescript/module/api/userClient.d.ts +11 -0
  117. package/lib/typescript/module/auth/authStore.d.ts +29 -6
  118. package/lib/typescript/module/config/index.d.ts +6 -0
  119. package/lib/typescript/module/deepLink/index.d.ts +54 -0
  120. package/lib/typescript/module/download/adapters/expoFs.d.ts +45 -0
  121. package/lib/typescript/module/download/adapters/shared.d.ts +3 -0
  122. package/lib/typescript/module/download/index.d.ts +76 -0
  123. package/lib/typescript/module/formatters/index.d.ts +2 -1
  124. package/lib/typescript/module/formatters/phone.d.ts +16 -1
  125. package/lib/typescript/module/geolocation/adapters/expoLocation.d.ts +42 -0
  126. package/lib/typescript/module/geolocation/index.d.ts +89 -0
  127. package/lib/typescript/module/index.d.ts +30 -13
  128. package/lib/typescript/module/initSDK.d.ts +32 -0
  129. package/lib/typescript/module/logger/index.d.ts +39 -0
  130. package/lib/typescript/module/network/index.d.ts +2 -2
  131. package/lib/typescript/module/network/networkStatus.d.ts +16 -0
  132. package/lib/typescript/module/permissions/index.d.ts +35 -0
  133. package/lib/typescript/module/sqlite/adapters/expo.d.ts +30 -0
  134. package/lib/typescript/module/sqlite/adapters/op.d.ts +27 -0
  135. package/lib/typescript/module/sqlite/adapters/rows.d.ts +7 -0
  136. package/lib/typescript/module/sqlite/adapters/storage.d.ts +30 -0
  137. package/lib/typescript/module/sqlite/adapters/sync.d.ts +31 -0
  138. package/lib/typescript/module/sqlite/index.d.ts +121 -0
  139. package/lib/typescript/module/sqlite/nitro.d.ts +7 -0
  140. package/lib/typescript/module/sqlite/op.d.ts +8 -0
  141. package/lib/typescript/module/sqlite/quick.d.ts +4 -0
  142. package/lib/typescript/module/sqlite/storage.d.ts +4 -0
  143. package/lib/typescript/module/storage/index.d.ts +24 -2
  144. package/lib/typescript/module/utils/imageCompression.d.ts +17 -0
  145. package/lib/typescript/module/utils/index.d.ts +2 -2
  146. package/package.json +102 -14
@@ -1,38 +1,83 @@
1
1
  "use strict";
2
2
 
3
+ import * as SecureStore from 'expo-secure-store';
3
4
  import { Logger } from "../logger/index.js";
4
5
 
5
6
  /**
6
7
  * Token data persisted in the keychain.
7
8
  */
9
+ /**
10
+ * The two keys the library itself reads. Everything in the library that needs
11
+ * the bearer token, the HTTP client, the download module and the authenticated
12
+ * image cache, resolves it through these rather than repeating the literal, so
13
+ * one of them cannot silently drift and start reading a key nothing writes.
14
+ * Apps are free to store their own tokens under any other key.
15
+ */
16
+ export const ACCESS_TOKEN_KEY = 'accessToken';
17
+ export const REFRESH_TOKEN_KEY = 'refreshToken';
8
18
 
9
19
  /**
10
20
  * Minimal keychain shape we depend on.
11
21
  * Lets us avoid a hard import on `react-native-keychain`, so an app that never
12
22
  * stores a token does not have to install it.
13
23
  */
24
+ /**
25
+ * The GENERIC password API, not the internet-credentials one.
26
+ *
27
+ * `kSecClassInternetPassword` exists for credentials bound to a server, protocol
28
+ * and port, and it integrates with Safari and AutoFill. A bearer token keyed by
29
+ * a name like "accessToken" is none of those things, and the library was only
30
+ * satisfying that API's required `server` argument by passing the key twice,
31
+ * once as the server and once as the service. That is misuse of the class.
32
+ *
33
+ * `kSecClassGenericPassword` is the class for app-local secrets, which is what a
34
+ * token is. It is what expo-secure-store uses underneath, and what the consumer
35
+ * app being migrated had already chosen for itself.
36
+ */
14
37
 
38
+ // Only what a test or the showcase injected; null means use expo-secure-store.
15
39
  let backend = null;
16
- let resolved = false;
17
- const resolveBackend = () => {
18
- if (resolved) {
19
- return backend;
20
- }
21
- resolved = true;
22
- try {
23
- const mod = require('react-native-keychain');
24
- // Prefer named exports: the real module and the test mock both expose them
25
- // flat, while `default` is a differently-shaped convenience object.
26
- backend = typeof mod?.setInternetCredentials === 'function' ? mod : mod?.default;
27
- } catch {
28
- backend = null;
40
+
41
+ /**
42
+ * expo-secure-store, adapted to the generic-password shape this module speaks.
43
+ *
44
+ * It is a flat key/value store, so the credential shape is reconstructed around
45
+ * it. The key is sanitised because expo rejects anything outside
46
+ * [A-Za-z0-9._-] at runtime, which would otherwise surface as a throw from deep
47
+ * inside the store rather than as a bad key.
48
+ */
49
+ const keyFor = service => (service ?? 'default').replace(/[^A-Za-z0-9._-]/g, '_');
50
+ const expoSecureStoreBackend = {
51
+ setGenericPassword: async (username, password, options) => {
52
+ await SecureStore.setItemAsync(keyFor(options?.service), JSON.stringify({
53
+ username,
54
+ password
55
+ }));
56
+ return true;
57
+ },
58
+ getGenericPassword: async options => {
59
+ const service = options?.service;
60
+ const raw = await SecureStore.getItemAsync(keyFor(service));
61
+ if (raw === null) {
62
+ return false;
63
+ }
64
+ const parsed = JSON.parse(raw);
65
+ return {
66
+ username: parsed.username,
67
+ password: parsed.password,
68
+ service: service ?? 'default'
69
+ };
70
+ },
71
+ resetGenericPassword: async options => {
72
+ await SecureStore.deleteItemAsync(keyFor(options?.service));
73
+ return true;
29
74
  }
30
- return backend;
31
75
  };
76
+ const resolveBackend = () => backend ?? expoSecureStoreBackend;
32
77
  const requireBackend = op => {
33
78
  const b = resolveBackend();
34
79
  if (!b) {
35
- const err = new Error(`[authStore] react-native-keychain is not installed; cannot ${op}. ` + 'Install the peer dep or call setKeychainImplementation() with a custom backend.');
80
+ const err = new Error(`[authStore] no secure-store backend available; cannot ${op}.`);
36
81
  Logger.error(err.message);
37
82
  throw err;
38
83
  }
@@ -40,12 +85,12 @@ const requireBackend = op => {
40
85
  };
41
86
 
42
87
  /**
43
- * Inject a custom keychain backend at runtime.
44
- * Pass `null` to reset to the default react-native-keychain backend.
88
+ * Replace the secure-store backend. Intended for TESTS and for the showcase,
89
+ * which injects a failing store to demo the write-failure path; it is not how
90
+ * an app configures the library. Pass `null` to restore expo-secure-store.
45
91
  */
46
92
  export const setKeychainImplementation = impl => {
47
93
  backend = impl;
48
- resolved = impl !== null;
49
94
  };
50
95
 
51
96
  /**
@@ -60,9 +105,9 @@ export const storeToken = async (key, token, expiry) => {
60
105
  token,
61
106
  expiry
62
107
  });
63
- await requireBackend('store a token').setInternetCredentials(key, 'token', data, {
108
+ await requireBackend('store a token').setGenericPassword('token', data, {
64
109
  service: key
65
- }); // Ensure service matches
110
+ });
66
111
  Logger.info(`Token stored successfully for key: ${key}`);
67
112
  } catch (error) {
68
113
  // Log AND rethrow. Swallowing this told the caller the token was persisted
@@ -90,8 +135,10 @@ const parseTokenData = credentials => {
90
135
  return parsed;
91
136
  }
92
137
  return null;
93
- } catch (error) {
94
- Logger.error('Failed to parse token data', error);
138
+ } catch {
139
+ // The single report for "stored, but not a token" belongs to getToken, which
140
+ // knows whether anything was stored at all. Logging here too made one event
141
+ // arrive twice, at two different levels.
95
142
  return null;
96
143
  }
97
144
  };
@@ -100,31 +147,43 @@ const parseTokenData = credentials => {
100
147
  * Retrieve token and check if expired.
101
148
  */
102
149
  export const getToken = async key => {
103
- Logger.info(`Retrieving token for key: ${key}`);
104
150
  try {
105
- const credentials = await requireBackend('read a token').getInternetCredentials(key, {
151
+ const credentials = await requireBackend('read a token').getGenericPassword({
106
152
  service: key
107
- }); // Ensure service matches
153
+ });
154
+ // Nothing stored is an ORDINARY state, not a fault: a signed-out user has no
155
+ // token, and every authenticated request, image and download asks for one.
156
+ // Warning here put three info lines and a warning into the log, and into the
157
+ // remote crash sink, on every one of those calls.
158
+ if (!credentials) {
159
+ return null;
160
+ }
108
161
  const parsedData = parseTokenData(credentials);
109
- Logger.info(`Parsed token data for key: ${key}: ${parsedData ? 'present' : 'absent'}`);
110
162
  if (!parsedData) {
111
- Logger.warn(`Invalid or no token data found for key: ${key}`);
163
+ // Something IS stored and it is not a token. That is corruption worth
164
+ // hearing about, and it is a different event from having no token.
165
+ Logger.warn(`Stored credentials for key "${key}" are not a valid token; ignoring them.`);
112
166
  return null;
113
167
  }
114
168
  const {
115
169
  token,
116
170
  expiry
117
171
  } = parsedData;
118
- Logger.info(`Token retrieved for key: ${key}`);
119
172
  return {
120
173
  token,
121
174
  expiry
122
175
  };
123
176
  } catch (error) {
124
177
  const message = error instanceof Error ? error.message : String(error);
125
- if (message.includes('Key permanently invalidated')) {
126
- Logger.error(`Key permanently invalidated for key: ${key}. Clearing the invalidated key.`);
127
- await removeToken(key); // Clear the invalidated key
178
+ // Both spellings matter. Android throws "Key permanently invalidated" when
179
+ // the biometric enrolment changes; iOS throws "Secret invalidated" for the
180
+ // equivalent. Either way the stored value can never be decrypted again, so
181
+ // clearing it is the only way out: leaving it makes every later read fail
182
+ // identically, and the user is stuck with no route back to a login. A
183
+ // consumer app handled the iOS spelling and the library did not.
184
+ if (message.includes('Key permanently invalidated') || message.includes('Secret invalidated')) {
185
+ Logger.error(`Stored token for key "${key}" can no longer be decrypted; clearing it.`);
186
+ await removeToken(key);
128
187
  return null;
129
188
  }
130
189
  Logger.error(`Error retrieving token for key: ${key}`, error);
@@ -137,11 +196,19 @@ export const getToken = async key => {
137
196
  */
138
197
  export const removeToken = async key => {
139
198
  try {
140
- await requireBackend('remove a token').resetInternetCredentials({
199
+ await requireBackend('remove a token').resetGenericPassword({
141
200
  service: key
142
201
  });
143
202
  Logger.info(`Token removed successfully for key: ${key}`);
144
203
  } catch (error) {
204
+ // Clearing a key that was never written is the ordinary logout path, not a
205
+ // failure: only one of the two tokens may ever have been stored. Logging it
206
+ // as an error puts routine traffic in the channel people watch for real
207
+ // problems, which is how an error channel stops being watched.
208
+ if (error instanceof Error && error.message.includes('Item not found')) {
209
+ Logger.info(`No token found to remove for key: ${key}`);
210
+ return;
211
+ }
145
212
  Logger.error(`Failed to remove token for key: ${key}`, error);
146
213
  }
147
214
  };
@@ -11,16 +11,40 @@ const base64UrlToBase64 = input => {
11
11
  const padded = input + '='.repeat((4 - input.length % 4) % 4);
12
12
  return padded.replace(/-/g, '+').replace(/_/g, '/');
13
13
  };
14
- const decodeBase64 = input => {
15
- // RN runtimes expose `atob` (Hermes 0.71+); fall back to a Buffer-based decode otherwise.
16
- if (typeof atob === 'function') {
17
- return atob(input);
14
+ const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
15
+
16
+ /**
17
+ * Pure-JS base64, used only when the runtime has no `atob`.
18
+ *
19
+ * This used to be `require('buffer')`. That was a latent bundling failure for
20
+ * every consumer: `buffer` is a Node builtin that React Native does not
21
+ * provide, it was never declared as a dependency, and Metro resolves requires
22
+ * STATICALLY — so sitting inside an unreached fallback branch did not save it.
23
+ * The app still failed to bundle, with an error naming this file. It only went
24
+ * unnoticed because the repo's own checks never bundle for a device.
25
+ *
26
+ * Decoding four base64 characters yields three bytes; the padding tells us how
27
+ * many of those three are real.
28
+ */
29
+ const decodeBase64Fallback = input => {
30
+ let out = '';
31
+ for (let i = 0; i < input.length; i += 4) {
32
+ const chunk = [0, 1, 2, 3].map(offset => BASE64_ALPHABET.indexOf(input[i + offset] ?? '='));
33
+ const bits = Math.max(chunk[0], 0) << 18 | Math.max(chunk[1], 0) << 12 | Math.max(chunk[2], 0) << 6 | Math.max(chunk[3], 0);
34
+ out += String.fromCharCode(bits >> 16 & 0xff);
35
+ if (chunk[2] !== -1) {
36
+ out += String.fromCharCode(bits >> 8 & 0xff);
37
+ }
38
+ if (chunk[3] !== -1) {
39
+ out += String.fromCharCode(bits & 0xff);
40
+ }
18
41
  }
19
- const {
20
- Buffer
21
- } = require('buffer');
22
- return Buffer.from(input, 'base64').toString('utf-8');
42
+ return out;
23
43
  };
44
+ const decodeBase64 = input =>
45
+ // Every RN runtime the library supports has `atob` (Hermes 0.71+), so the
46
+ // fallback is a safety net rather than a path anyone is expected to take.
47
+ typeof atob === 'function' ? atob(input) : decodeBase64Fallback(input);
24
48
  const utf8FromBinary = binary => {
25
49
  // Convert percent-encoded UTF-8 sequence so multi-byte chars survive.
26
50
  try {
@@ -12,7 +12,8 @@ const DEFAULT_CONFIG = {
12
12
  environment: 'Development',
13
13
  country: 'in',
14
14
  appId: null,
15
- appVersion: null
15
+ appVersion: null,
16
+ logCurl: false
16
17
  };
17
18
  let globalConfig = {
18
19
  ...DEFAULT_CONFIG
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { Linking } from 'react-native';
5
+ import { Logger } from "../logger/index.js";
6
+ const PREFIX = '[@webority-technologies/mobile-core]';
7
+ const decodeSafe = value => {
8
+ try {
9
+ return decodeURIComponent(value.replace(/\+/g, ' '));
10
+ } catch {
11
+ return value;
12
+ }
13
+ };
14
+ const parseQuery = raw => {
15
+ const out = {};
16
+ if (!raw) {
17
+ return out;
18
+ }
19
+ for (const pair of raw.split('&')) {
20
+ if (!pair) {
21
+ continue;
22
+ }
23
+ const eq = pair.indexOf('=');
24
+ const key = decodeSafe(eq >= 0 ? pair.slice(0, eq) : pair);
25
+ if (!key) {
26
+ continue;
27
+ }
28
+ out[key] = eq >= 0 ? decodeSafe(pair.slice(eq + 1)) : '';
29
+ }
30
+ return out;
31
+ };
32
+ const trimSlashes = value => value.replace(/^\/+/, '').replace(/\/+$/, '');
33
+
34
+ /**
35
+ * Parse a deep-link URL. Returns null for anything that is not a usable URL so
36
+ * a hostile or truncated link can never throw on the native event path.
37
+ */
38
+ export const parseDeepLink = url => {
39
+ if (typeof url !== 'string') {
40
+ return null;
41
+ }
42
+ const trimmed = url.trim();
43
+ if (!trimmed || /\s/.test(trimmed)) {
44
+ return null;
45
+ }
46
+ const withoutFragment = trimmed.split('#')[0] ?? '';
47
+ const schemeEnd = withoutFragment.indexOf('://');
48
+ let scheme = '';
49
+ let rest = withoutFragment;
50
+ if (schemeEnd >= 0) {
51
+ const rawScheme = withoutFragment.slice(0, schemeEnd);
52
+ if (!/^[a-zA-Z][a-zA-Z0-9+.-]*$/.test(rawScheme)) {
53
+ return null;
54
+ }
55
+ scheme = rawScheme.toLowerCase();
56
+ rest = withoutFragment.slice(schemeEnd + 3);
57
+ }
58
+ const queryStart = rest.indexOf('?');
59
+ const rawPath = queryStart >= 0 ? rest.slice(0, queryStart) : rest;
60
+ const query = parseQuery(queryStart >= 0 ? rest.slice(queryStart + 1) : '');
61
+ let host = '';
62
+ let pathPart = rawPath;
63
+ if (scheme === 'http' || scheme === 'https') {
64
+ const slash = rawPath.indexOf('/');
65
+ host = slash >= 0 ? rawPath.slice(0, slash) : rawPath;
66
+ pathPart = slash >= 0 ? rawPath.slice(slash + 1) : '';
67
+ }
68
+ const path = trimSlashes(pathPart);
69
+ const segments = path ? path.split('/').filter(Boolean) : [];
70
+ if (scheme && scheme !== 'http' && scheme !== 'https') {
71
+ host = segments[0] ?? '';
72
+ }
73
+ return {
74
+ url: trimmed,
75
+ scheme,
76
+ host,
77
+ path,
78
+ segments,
79
+ query
80
+ };
81
+ };
82
+ const matchPattern = (pattern, segments) => {
83
+ const patternSegments = trimSlashes(pattern).split('/').filter(Boolean);
84
+ if (patternSegments.length !== segments.length) {
85
+ return null;
86
+ }
87
+ const params = {};
88
+ for (let i = 0; i < patternSegments.length; i += 1) {
89
+ const p = patternSegments[i];
90
+ const s = segments[i];
91
+ if (p.startsWith(':')) {
92
+ const name = p.slice(1);
93
+ if (!name) {
94
+ return null;
95
+ }
96
+ params[name] = s;
97
+ } else if (p.toLowerCase() !== s.toLowerCase()) {
98
+ return null;
99
+ }
100
+ }
101
+ return params;
102
+ };
103
+ const matchRegExp = (pattern, path) => {
104
+ // A /g or /y pattern carries lastIndex between calls, so the same link would
105
+ // match on one dispatch and miss on the next.
106
+ if (pattern.global || pattern.sticky) {
107
+ pattern.lastIndex = 0;
108
+ }
109
+ const result = pattern.exec(path);
110
+ if (!result) {
111
+ return null;
112
+ }
113
+ const params = {};
114
+ for (let i = 1; i < result.length; i += 1) {
115
+ const group = result[i];
116
+ if (group !== undefined) {
117
+ params[String(i)] = group;
118
+ }
119
+ }
120
+ for (const [name, value] of Object.entries(result.groups ?? {})) {
121
+ if (value !== undefined) {
122
+ params[name] = value;
123
+ }
124
+ }
125
+ return params;
126
+ };
127
+ const matchRoute = (routes, link) => {
128
+ for (const route of routes) {
129
+ const params = typeof route.match === 'string' ? matchPattern(route.match, link.segments) : matchRegExp(route.match, link.path);
130
+ if (params) {
131
+ return {
132
+ route,
133
+ params
134
+ };
135
+ }
136
+ }
137
+ return null;
138
+ };
139
+ let config = null;
140
+ let subscription = null;
141
+ let started = false;
142
+ let pending = null;
143
+ let readyOverride = null;
144
+ const isReady = () => {
145
+ if (readyOverride !== null) {
146
+ return readyOverride;
147
+ }
148
+ return config?.isReady?.() ?? true;
149
+ };
150
+ const isAuthenticated = () => config?.isAuthenticated?.() ?? true;
151
+ const dispatch = link => {
152
+ const active = config;
153
+ if (!active) {
154
+ return;
155
+ }
156
+ const matched = matchRoute(active.routes, link);
157
+ if (!matched) {
158
+ active.onUnmatched?.(link);
159
+ return;
160
+ }
161
+ if (matched.route.requiresAuth && !isAuthenticated()) {
162
+ pending = link;
163
+ active.onUnauthenticated?.(link);
164
+ return;
165
+ }
166
+ matched.route.handler(link, matched.params);
167
+ };
168
+ const flushPending = () => {
169
+ const link = pending;
170
+ if (!link) {
171
+ return;
172
+ }
173
+ // Cleared BEFORE dispatch: a handler that navigates can re-enter this path,
174
+ // and a link that fires twice is the classic deep-link duplicate-screen bug.
175
+ pending = null;
176
+ dispatch(link);
177
+ };
178
+ const configure = next => {
179
+ if (!next || !Array.isArray(next.routes)) {
180
+ throw new Error(`${PREFIX} DeepLink.configure() needs a config object with a "routes" array. ` + 'Pass { routes: [{ match: "order/:id", handler }] }.');
181
+ }
182
+ config = next;
183
+ };
184
+ const handleUrl = url => {
185
+ if (!config) {
186
+ Logger.error(`${PREFIX} DeepLink received a URL before configure() was called; the link was dropped. ` + 'Call DeepLink.configure({ routes }) (or useDeepLink) during app start.');
187
+ return;
188
+ }
189
+ const parsed = parseDeepLink(url);
190
+ // A malformed URL still reaches the app through onUnmatched, with the raw
191
+ // value in `url`, rather than vanishing.
192
+ if (!parsed) {
193
+ config.onUnmatched?.({
194
+ url: typeof url === 'string' ? url : '',
195
+ scheme: '',
196
+ host: '',
197
+ path: '',
198
+ segments: [],
199
+ query: {}
200
+ });
201
+ return;
202
+ }
203
+ const link = parsed;
204
+ if (!isReady()) {
205
+ pending = link;
206
+ return;
207
+ }
208
+ dispatch(link);
209
+ };
210
+ const start = async () => {
211
+ if (!config) {
212
+ throw new Error(`${PREFIX} DeepLink.start() was called before configure(). ` + 'Call DeepLink.configure({ routes }) first, or use the useDeepLink() hook.');
213
+ }
214
+ if (started) {
215
+ return;
216
+ }
217
+ started = true;
218
+ // Subscribe before reading the cold-start URL so a link arriving during the
219
+ // await is not lost.
220
+ subscription = Linking.addEventListener('url', event => {
221
+ handleUrl(event.url);
222
+ });
223
+ try {
224
+ const initial = await Linking.getInitialURL();
225
+ if (initial) {
226
+ handleUrl(initial);
227
+ }
228
+ } catch (error) {
229
+ Logger.error(`${PREFIX} DeepLink could not read the cold-start URL`, error);
230
+ }
231
+ };
232
+ const stop = () => {
233
+ subscription?.remove?.();
234
+ subscription = null;
235
+ started = false;
236
+ pending = null;
237
+ readyOverride = null;
238
+ };
239
+ const setReady = ready => {
240
+ readyOverride = ready;
241
+ if (ready) {
242
+ flushPending();
243
+ }
244
+ };
245
+ export const DeepLink = {
246
+ configure,
247
+ start,
248
+ stop,
249
+ handleUrl,
250
+ setReady,
251
+ /** Replay the stashed link, once. Call after a successful login. */
252
+ replayPending: flushPending,
253
+ getPending: () => pending
254
+ };
255
+
256
+ /**
257
+ * Configure and start the deep-link service for the lifetime of a component.
258
+ * The stored config delegates to the latest render, so new route closures do
259
+ * not tear down and re-subscribe the native listener.
260
+ */
261
+ export const useDeepLink = deepLinkConfig => {
262
+ const configRef = useRef(deepLinkConfig);
263
+ configRef.current = deepLinkConfig;
264
+ useEffect(() => {
265
+ DeepLink.configure({
266
+ get routes() {
267
+ return configRef.current.routes;
268
+ },
269
+ isReady: () => configRef.current.isReady?.() ?? true,
270
+ isAuthenticated: () => configRef.current.isAuthenticated?.() ?? true,
271
+ onUnauthenticated: link => configRef.current.onUnauthenticated?.(link),
272
+ onUnmatched: link => configRef.current.onUnmatched?.(link)
273
+ });
274
+ void DeepLink.start();
275
+ return () => {
276
+ DeepLink.stop();
277
+ };
278
+ }, []);
279
+ };
280
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * expo-file-system. Shape verified against 57.0.6, where the legacy function
5
+ * API (`downloadAsync`, `getInfoAsync`) still exists on the main entry point
6
+ * but every one of those functions THROWS at runtime; they now live behind
7
+ * `expo-file-system/legacy`. This adapter therefore uses the class API only.
8
+ *
9
+ * The REQUIRE lives in the entry point beside this file, never here.
10
+ */
11
+
12
+ /**
13
+ * expo reports a directory as a URI with a trailing slash, and the download
14
+ * module joins a directory to a file name with a `/` of its own.
15
+ */
16
+ const asDirectoryPath = uri => uri.replace(/\/+$/, '');
17
+
18
+ /**
19
+ * expo REJECTS a non-2xx download rather than resolving a status, and the two
20
+ * platforms word it differently: iOS "server returned HTTP 404", Android
21
+ * "HTTP 404", and the non-task download path "response has status: 404".
22
+ * Recovering the code is what lets the download module report `http-error` with
23
+ * a status rather than a bare `network` failure. A future wording change
24
+ * degrades to `network`, never to a wrong status.
25
+ */
26
+ const HTTP_STATUS = /(?:HTTP|status)\s*:?\s*(\d{3})\b/i;
27
+ const statusFrom = error => {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ const match = HTTP_STATUS.exec(message);
30
+ return match ? Number(match[1]) : undefined;
31
+ };
32
+ export const adaptExpoFileSystem = mod => {
33
+ const lib = mod?.default ?? mod;
34
+ if (typeof lib?.File !== 'function' || typeof lib.File.createDownloadTask !== 'function') {
35
+ return null;
36
+ }
37
+ const {
38
+ File,
39
+ Directory,
40
+ Paths
41
+ } = lib;
42
+ const documents = asDirectoryPath(Paths.document.uri);
43
+ return {
44
+ dirs: {
45
+ documents,
46
+ cache: asDirectoryPath(Paths.cache.uri),
47
+ // expo exposes no public Downloads directory, so a "downloads" request
48
+ // lands in the app's documents directory instead of the shared folder.
49
+ downloads: documents
50
+ },
51
+ download: ({
52
+ url,
53
+ path,
54
+ headers,
55
+ onProgress
56
+ }) => {
57
+ const task = File.createDownloadTask(url, new File(path), {
58
+ headers,
59
+ onProgress: onProgress ? progress => onProgress(progress.bytesWritten, progress.totalBytes) : undefined
60
+ });
61
+ return {
62
+ promise: task.downloadAsync().then(file => ({
63
+ bytes: file?.size
64
+ }), error => {
65
+ const status = statusFrom(error);
66
+ if (status === undefined) {
67
+ throw error;
68
+ }
69
+ return {
70
+ status
71
+ };
72
+ }),
73
+ cancel: () => task.cancel()
74
+ };
75
+ },
76
+ exists: path => Promise.resolve(new File(path).exists),
77
+ stat: path => Promise.resolve({
78
+ size: new File(path).size
79
+ }),
80
+ unlink: path => {
81
+ new File(path).delete();
82
+ return Promise.resolve();
83
+ },
84
+ // The download module calls this for a directory that usually already
85
+ // exists, so it must be idempotent rather than throw on the second run.
86
+ mkdir: path => {
87
+ new Directory(path).create({
88
+ intermediates: true,
89
+ idempotent: true
90
+ });
91
+ return Promise.resolve();
92
+ }
93
+ };
94
+ };
95
+ //# sourceMappingURL=expoFs.js.map
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+
3
+ /** Coerce a driver-supplied size, which some report as a string. */
4
+ export const asNumber = value => {
5
+ const n = typeof value === 'number' ? value : Number(value);
6
+ return Number.isFinite(n) ? n : 0;
7
+ };
8
+ //# sourceMappingURL=shared.js.map