@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
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+
3
+ import * as ExpoFileSystem from 'expo-file-system';
4
+ import { Linking, Platform } from 'react-native';
5
+ import { ACCESS_TOKEN_KEY, getToken } from "../auth/authStore.js";
6
+ import { Logger } from "../logger/index.js";
7
+ import { Permissions } from "../permissions/index.js";
8
+ import { adaptExpoFileSystem } from "./adapters/expoFs.js";
9
+ import { asNumber } from "./adapters/shared.js";
10
+
11
+ /**
12
+ * This module names NO native package, for the same reason as the SQLite core.
13
+ * The app wires a filesystem once at startup:
14
+ *
15
+ * import { setFileSystemImplementation } from '@webority-technologies/mobile-core/download';
16
+ * import { blobUtilFileSystem } from '@webority-technologies/mobile-core/download/blob-util';
17
+ * setFileSystemImplementation(blobUtilFileSystem());
18
+ */
19
+
20
+ const PREFIX = '[@webority-technologies/mobile-core]';
21
+
22
+ /** Android stopped requiring a storage permission for a Downloads write at API 29. */
23
+ const SCOPED_STORAGE_API_LEVEL = 29;
24
+
25
+ /**
26
+ * The slice of a native filesystem module this module actually uses. Both
27
+ * `react-native-blob-util` and `react-native-fs` are adapted onto it, and an app
28
+ * can inject its own with `setFileSystemImplementation`.
29
+ */
30
+
31
+ export class DownloadError extends Error {
32
+ constructor(code, message, status) {
33
+ super(`${PREFIX} ${message}`);
34
+ this.name = 'DownloadError';
35
+ this.code = code;
36
+ this.status = status;
37
+ // Babel's class transform can drop the prototype link, which makes
38
+ // `instanceof DownloadError` false in the transpiled build.
39
+ Object.setPrototypeOf(this, DownloadError.prototype);
40
+ }
41
+ }
42
+ const describe = error => error instanceof Error ? error.message : String(error);
43
+
44
+ /** Only what a test or the showcase injected; null means use expo-file-system. */
45
+ let backend = null;
46
+
47
+ /**
48
+ * expo-file-system, adapted once and memoised. The adapter is shape-checked, so
49
+ * a build where the native module did not link returns null and the guided
50
+ * error below still fires rather than a confusing property access.
51
+ */
52
+ let expoBackend;
53
+ const resolveBackend = () => {
54
+ if (backend) {
55
+ return backend;
56
+ }
57
+ if (expoBackend === undefined) {
58
+ expoBackend = adaptExpoFileSystem(ExpoFileSystem);
59
+ }
60
+ return expoBackend;
61
+ };
62
+
63
+ /**
64
+ * Replace the filesystem backend. Intended for TESTS and for the showcase,
65
+ * which injects a denying backend to demo the permission error; an app gets
66
+ * expo-file-system without wiring anything. Pass `null` to restore it.
67
+ */
68
+ export const setFileSystemImplementation = impl => {
69
+ backend = impl;
70
+ };
71
+ const requireBackend = () => {
72
+ const fs = resolveBackend();
73
+ if (!fs) {
74
+ throw new DownloadError('unsupported', 'No filesystem module is available, so files cannot be downloaded. ' + 'expo-file-system did not load, which on a real build means the native module ' + 'was not linked.');
75
+ }
76
+ return fs;
77
+ };
78
+ const stripControlChars = value => Array.from(value).filter(char => (char.codePointAt(0) ?? 0) > 0x1f).join('');
79
+ const lastSegment = raw => {
80
+ const segments = raw.split(/[/\\]/);
81
+ return stripControlChars(segments[segments.length - 1] ?? '').trim();
82
+ };
83
+
84
+ /**
85
+ * Reduce a caller- or server-supplied name to a single path segment: `../../evil.pdf`
86
+ * must land inside the destination directory as `evil.pdf` rather than escaping it.
87
+ */
88
+ const sanitiseFileName = raw => {
89
+ const base = lastSegment(raw);
90
+ if (base === '' || base === '.' || base === '..') {
91
+ throw new DownloadError('write-failed', `"${raw}" does not contain a usable file name. Pass an explicit fileName.`);
92
+ }
93
+ return base;
94
+ };
95
+ const fileNameFromUrl = url => {
96
+ const withoutQuery = url.split('#')[0]?.split('?')[0] ?? '';
97
+ const last = withoutQuery.split('/').pop() ?? '';
98
+ let decoded = last;
99
+ try {
100
+ decoded = decodeURIComponent(last);
101
+ } catch {
102
+ // A malformed escape sequence is not a reason to fail the download.
103
+ }
104
+ const base = lastSegment(decoded);
105
+ return base === '' || base === '.' || base === '..' ? 'download' : base;
106
+ };
107
+ const androidApiLevel = () => typeof Platform.Version === 'number' ? Platform.Version : Number.parseInt(String(Platform.Version), 10);
108
+ const ensureStoragePermission = async destination => {
109
+ if (Platform.OS !== 'android' || destination !== 'downloads') {
110
+ return;
111
+ }
112
+ if (androidApiLevel() >= SCOPED_STORAGE_API_LEVEL) {
113
+ return;
114
+ }
115
+ const status = await Permissions.ensure('storage');
116
+ if (status === 'granted' || status === 'limited') {
117
+ return;
118
+ }
119
+ throw new DownloadError('permission-denied', `Storage permission is "${status}", so nothing can be written to the Downloads folder ` + 'on this Android version. Ask the user to grant it (Permissions.openSettings()), or ' + 'download to the "documents" destination instead.');
120
+ };
121
+ const buildHeaders = async options => {
122
+ const headers = {
123
+ ...options.headers
124
+ };
125
+ if (options.authenticated === false) {
126
+ return headers;
127
+ }
128
+ const stored = await getToken(ACCESS_TOKEN_KEY);
129
+ if (stored?.token) {
130
+ headers.Authorization = `Bearer ${stored.token}`;
131
+ }
132
+ return headers;
133
+ };
134
+ const discardPartial = async (fs, path) => {
135
+ try {
136
+ if (await fs.exists(path)) {
137
+ await fs.unlink(path);
138
+ }
139
+ } catch (error) {
140
+ Logger.warn(`${PREFIX} Failed to remove the partial download at ${path}`, error);
141
+ }
142
+ };
143
+
144
+ /**
145
+ * Download a file to the device, attaching the stored bearer token by default.
146
+ *
147
+ * A finished download is always verified to be non-empty: a 0-byte file looks
148
+ * like a success to every caller and then wedges whatever consumes it, so it is
149
+ * deleted and reported as `empty-file`.
150
+ */
151
+ export const downloadFile = async options => {
152
+ const fs = requireBackend();
153
+ const destination = options.destination ?? 'documents';
154
+ const fileName = options.fileName ? sanitiseFileName(options.fileName) : fileNameFromUrl(options.url);
155
+ await ensureStoragePermission(destination);
156
+ const directory = fs.dirs[destination];
157
+ const path = `${directory}/${fileName}`;
158
+ if (options.signal?.aborted) {
159
+ throw new DownloadError('aborted', `The download of ${fileName} was aborted before it started.`);
160
+ }
161
+ let reuseExisting = false;
162
+ try {
163
+ await fs.mkdir(directory);
164
+ const exists = await fs.exists(path);
165
+ if (exists && options.overwrite === false) {
166
+ reuseExisting = true;
167
+ } else if (exists) {
168
+ await fs.unlink(path);
169
+ }
170
+ } catch (error) {
171
+ throw new DownloadError('write-failed', `Could not prepare ${path} for writing: ${describe(error)}`);
172
+ }
173
+ if (reuseExisting) {
174
+ const stat = await fs.stat(path);
175
+ return {
176
+ path,
177
+ bytes: asNumber(stat?.size),
178
+ mimeType: options.mimeType,
179
+ fromCache: true
180
+ };
181
+ }
182
+ const headers = await buildHeaders(options);
183
+ const {
184
+ onProgress
185
+ } = options;
186
+ const handle = fs.download({
187
+ url: options.url,
188
+ path,
189
+ headers,
190
+ onProgress: onProgress ? (received, total) => onProgress(received, total, total > 0 ? received / total : 0) : undefined
191
+ });
192
+ let aborted = false;
193
+ let rejectOnAbort = null;
194
+ // Raced against the transfer rather than relied upon to settle it: cancelling a
195
+ // native download is best-effort, and a backend whose promise never settles
196
+ // after cancel would otherwise leave the caller waiting forever.
197
+ const abortRequested = new Promise((_, reject) => {
198
+ rejectOnAbort = () => reject(new DownloadError('aborted', `The download of ${fileName} was aborted.`));
199
+ });
200
+ const onAbort = () => {
201
+ aborted = true;
202
+ handle.cancel?.();
203
+ rejectOnAbort?.();
204
+ };
205
+ options.signal?.addEventListener('abort', onAbort);
206
+ // The signal can fire between the aborted-check above and this listener being
207
+ // attached (token read, mkdir); a listener added after the fact never fires.
208
+ if (options.signal?.aborted) {
209
+ onAbort();
210
+ }
211
+ let status;
212
+ try {
213
+ ({
214
+ status
215
+ } = await Promise.race([handle.promise, abortRequested]));
216
+ } catch (error) {
217
+ await discardPartial(fs, path);
218
+ if (aborted || options.signal?.aborted) {
219
+ throw new DownloadError('aborted', `The download of ${fileName} was aborted.`);
220
+ }
221
+ throw new DownloadError('network', `The download of ${options.url} failed: ${describe(error)}`);
222
+ } finally {
223
+ options.signal?.removeEventListener('abort', onAbort);
224
+ }
225
+ if (aborted || options.signal?.aborted) {
226
+ await discardPartial(fs, path);
227
+ throw new DownloadError('aborted', `The download of ${fileName} was aborted.`);
228
+ }
229
+ if (status !== undefined && (status < 200 || status >= 300)) {
230
+ await discardPartial(fs, path);
231
+ throw new DownloadError('http-error', `The server answered ${status} for ${options.url}, so no file was saved.`, status);
232
+ }
233
+ let bytes = 0;
234
+ try {
235
+ bytes = (await fs.exists(path)) ? asNumber((await fs.stat(path)).size) : 0;
236
+ } catch (error) {
237
+ await discardPartial(fs, path);
238
+ throw new DownloadError('write-failed', `The download of ${fileName} finished but ${path} could not be read back: ${describe(error)}`);
239
+ }
240
+ if (bytes <= 0) {
241
+ await discardPartial(fs, path);
242
+ throw new DownloadError('empty-file', `The download of ${options.url} produced an empty file, which has been deleted. ` + 'Check that the URL is correct and that the request carried the credentials the ' + 'server expects.');
243
+ }
244
+ return {
245
+ path,
246
+ bytes,
247
+ mimeType: options.mimeType,
248
+ fromCache: false
249
+ };
250
+ };
251
+ const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
252
+
253
+ /**
254
+ * Hand a downloaded file to the OS, to open in whichever app claims it.
255
+ */
256
+ export const openFile = async (path, mimeType) => {
257
+ const url = HAS_SCHEME.test(path) ? path : `file://${path}`;
258
+ try {
259
+ await Linking.openURL(url);
260
+ } catch (error) {
261
+ throw new DownloadError('unsupported', `No installed app could open ${url}${mimeType ? ` (${mimeType})` : ''}: ${describe(error)}`);
262
+ }
263
+ };
264
+ //# sourceMappingURL=index.js.map
@@ -142,7 +142,7 @@ export const formatRelativeTime = (value, base = new Date(), locale = 'en-IN') =
142
142
  unit = 'year';
143
143
  amount = Math.round(diffMs / YEAR);
144
144
  }
145
- const Rtf = Intl.RelativeTimeFormat;
145
+ const Rtf = hasIntl() ? Intl.RelativeTimeFormat : undefined;
146
146
  if (typeof Rtf === 'function') {
147
147
  try {
148
148
  return new Rtf(locale, {
@@ -4,5 +4,5 @@ export { formatCurrency } from "./currency.js";
4
4
  export { formatDate, formatDateTime, formatRelativeTime, formatTime } from "./date.js";
5
5
  export { getInitials } from "./initials.js";
6
6
  export { formatCompactNumber, formatNumber, formatPercent } from "./number.js";
7
- export { formatPhone, normalizePhone } from "./phone.js";
7
+ export { detectPhoneCountry, formatPhone, normalizePhone } from "./phone.js";
8
8
  //# sourceMappingURL=index.js.map
@@ -20,14 +20,90 @@ const groupIndianMobile = digits => {
20
20
  return `${digits.slice(0, 5)} ${digits.slice(5)}`;
21
21
  };
22
22
 
23
+ // Longest calling code first so a 3-digit code is never shadowed by a shorter one.
24
+ const COUNTRY_RULES = [{
25
+ country: 'AE',
26
+ callingCode: '971',
27
+ nationalLength: 9,
28
+ group: n => `${n.slice(0, 2)} ${n.slice(2, 5)} ${n.slice(5)}`
29
+ }, {
30
+ country: 'IN',
31
+ callingCode: '91',
32
+ nationalLength: 10,
33
+ group: groupIndianMobile
34
+ }, {
35
+ country: 'GB',
36
+ callingCode: '44',
37
+ nationalLength: 10,
38
+ group: n => `${n.slice(0, 4)} ${n.slice(4, 7)} ${n.slice(7)}`
39
+ }, {
40
+ country: 'AU',
41
+ callingCode: '61',
42
+ nationalLength: 9,
43
+ group: n => `${n.slice(0, 3)} ${n.slice(3, 6)} ${n.slice(6)}`
44
+ }, {
45
+ country: 'SG',
46
+ callingCode: '65',
47
+ nationalLength: 8,
48
+ group: n => `${n.slice(0, 4)} ${n.slice(4)}`
49
+ }, {
50
+ country: 'US',
51
+ callingCode: '1',
52
+ nationalLength: 10,
53
+ group: n => `(${n.slice(0, 3)}) ${n.slice(3, 6)}-${n.slice(6)}`
54
+ },
55
+ // US and CA share a calling code and are indistinguishable from it alone;
56
+ // this row only serves an explicit `country: 'CA'` override, never detection
57
+ // (it sits after the US row, and findRuleByCallingCode returns the first match).
58
+ {
59
+ country: 'CA',
60
+ callingCode: '1',
61
+ nationalLength: 10,
62
+ group: n => `(${n.slice(0, 3)}) ${n.slice(3, 6)}-${n.slice(6)}`
63
+ }];
64
+ const findRuleByCallingCode = rest => COUNTRY_RULES.find(rule => rest.startsWith(rule.callingCode));
65
+ const findRuleByCountry = country => COUNTRY_RULES.find(rule => rule.country === country);
66
+
67
+ /**
68
+ * Detect the country of a phone number: from its own `+` country code when
69
+ * present, otherwise from `defaultCountryCode`. Returns null when neither
70
+ * carries a recognised calling code.
71
+ */
72
+ export const detectPhoneCountry = (input, defaultCountryCode = '+91') => {
73
+ const normalized = normalizePhone(input);
74
+ if (!normalized) {
75
+ return null;
76
+ }
77
+ if (normalized.startsWith('+')) {
78
+ return findRuleByCallingCode(normalized.slice(1))?.country ?? null;
79
+ }
80
+ const normalizedDefault = normalizePhone(defaultCountryCode);
81
+ if (!normalizedDefault.startsWith('+')) {
82
+ return null;
83
+ }
84
+ return findRuleByCallingCode(normalizedDefault.slice(1))?.country ?? null;
85
+ };
86
+ const bestEffortGroup = rest => {
87
+ // No known country code matched (or matched but the wrong length): split
88
+ // the country code (first 1-3 digits) and group the rest in 4s.
89
+ const ccLen = rest.length > 11 ? 3 : rest.length > 10 ? 2 : 1;
90
+ const cc = rest.slice(0, ccLen);
91
+ const number = rest.slice(ccLen);
92
+ const grouped = number.replace(/(.{4})(?=.)/g, '$1 ').trim();
93
+ return `+${cc} ${grouped}`.trim();
94
+ };
95
+
23
96
  /**
24
97
  * Format a phone number for human display.
25
98
  *
26
99
  * - Indian mobile (10 digits) → `XXXXX XXXXX`
27
100
  * - With `+91` country code → `+91 XXXXX XXXXX`
101
+ * - A number that already carries a recognised country code is grouped for
102
+ * that country regardless of `defaultCountryCode`.
28
103
  * - Other inputs are returned as `+CC NNNN NNNN…` best-effort grouping.
29
104
  */
30
- export const formatPhone = (input, defaultCountryCode = '+91') => {
105
+
106
+ export function formatPhone(input, arg) {
31
107
  if (!input) {
32
108
  return '';
33
109
  }
@@ -35,20 +111,30 @@ export const formatPhone = (input, defaultCountryCode = '+91') => {
35
111
  if (!normalized) {
36
112
  return '';
37
113
  }
38
-
39
- // Already starts with +
114
+ const options = typeof arg === 'string' ? {} : arg ?? {};
115
+ const defaultCountryCode = typeof arg === 'string' ? arg : options.defaultCountryCode ?? '+91';
40
116
  if (normalized.startsWith('+')) {
41
117
  const rest = normalized.slice(1);
42
- // Indian
43
- if (rest.startsWith('91') && rest.length === 12) {
44
- return `+91 ${groupIndianMobile(rest.slice(2))}`;
118
+ const detectedRule = findRuleByCallingCode(rest);
119
+ const activeRule = options.country ? findRuleByCountry(options.country) : detectedRule;
120
+ if (activeRule) {
121
+ const national = detectedRule ? rest.slice(detectedRule.callingCode.length) : rest;
122
+ if (national.length === activeRule.nationalLength) {
123
+ const includeCc = options.includeCountryCode ?? true;
124
+ const body = activeRule.group(national);
125
+ return includeCc ? `+${activeRule.callingCode} ${body}` : body;
126
+ }
127
+ }
128
+ return bestEffortGroup(rest);
129
+ }
130
+ if (options.country) {
131
+ const rule = findRuleByCountry(options.country);
132
+ if (rule && normalized.length === rule.nationalLength) {
133
+ const includeCc = options.includeCountryCode ?? false;
134
+ const body = rule.group(normalized);
135
+ return includeCc ? `+${rule.callingCode} ${body}` : body;
45
136
  }
46
- // Generic — split country code (first 1–3 digits) and group the rest in 4s.
47
- const ccLen = rest.length > 11 ? 3 : rest.length > 10 ? 2 : 1;
48
- const cc = rest.slice(0, ccLen);
49
- const number = rest.slice(ccLen);
50
- const grouped = number.replace(/(.{4})(?=.)/g, '$1 ').trim();
51
- return `+${cc} ${grouped}`.trim();
137
+ return normalized.replace(/(.{4})(?=.)/g, '$1 ').trim();
52
138
  }
53
139
 
54
140
  // No country code; assume default if length matches Indian mobile.
@@ -57,5 +143,5 @@ export const formatPhone = (input, defaultCountryCode = '+91') => {
57
143
  }
58
144
  // Otherwise just return groups of 4.
59
145
  return normalized.replace(/(.{4})(?=.)/g, '$1 ').trim();
60
- };
146
+ }
61
147
  //# sourceMappingURL=phone.js.map
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The slice of expo-location this adapter uses. Verified against 57.0.14.
5
+ *
6
+ * The REQUIRE lives in the entry point beside this file, never here.
7
+ */
8
+
9
+ /**
10
+ * expo-location's Accuracy enum is numeric (`Balanced = 3`, `Highest = 5`,
11
+ * verified in Location.types.d.ts). The numbers are inlined rather than
12
+ * imported so this file names no expo module and stays bundlable by an app
13
+ * without expo installed.
14
+ */
15
+ const ACCURACY_BALANCED = 3;
16
+ const ACCURACY_HIGHEST = 5;
17
+ const toExpoOptions = options => {
18
+ const expo = {
19
+ accuracy: options?.enableHighAccuracy ? ACCURACY_HIGHEST : ACCURACY_BALANCED
20
+ };
21
+ if (typeof options?.distanceFilter === 'number') {
22
+ expo.distanceInterval = options.distanceFilter;
23
+ }
24
+ return expo;
25
+ };
26
+
27
+ /**
28
+ * expo-location rejects with an Error; the seam expects the W3C-style numeric
29
+ * codes the RN geolocation modules use, which the library then maps to its own
30
+ * GeolocationError kinds. Classifying here rather than passing the raw Error
31
+ * through is what makes an expo-backed app produce the same `permission-denied`
32
+ * / `services-disabled` / `timeout` values as a NetInfo-backed one — which is
33
+ * the whole point of having a seam.
34
+ */
35
+ const PERMISSION_PATTERN = /permission|denied|not (been )?granted|authoriz/i;
36
+ const SERVICES_PATTERN = /location services|provider|disabled|switched off|turned off/i;
37
+ const TIMEOUT_PATTERN = /time ?d? ?out|timeout/i;
38
+ const toNativeError = raw => {
39
+ const message = raw instanceof Error ? raw.message : String(raw);
40
+ if (PERMISSION_PATTERN.test(message)) {
41
+ return {
42
+ code: 1,
43
+ message
44
+ };
45
+ }
46
+ if (TIMEOUT_PATTERN.test(message)) {
47
+ return {
48
+ code: 3,
49
+ message
50
+ };
51
+ }
52
+ // Code 2 covers both "services off" and "no fix". The library separates them
53
+ // by reading the message, so the message must travel intact — and where expo
54
+ // does not say "location services", the wording is normalised so it can.
55
+ if (SERVICES_PATTERN.test(message)) {
56
+ return {
57
+ code: 2,
58
+ message
59
+ };
60
+ }
61
+ return {
62
+ code: 2,
63
+ message
64
+ };
65
+ };
66
+ const toNativePosition = location => ({
67
+ coords: {
68
+ latitude: location.coords?.latitude ?? null,
69
+ longitude: location.coords?.longitude ?? null,
70
+ accuracy: location.coords?.accuracy ?? null,
71
+ altitude: location.coords?.altitude ?? null,
72
+ heading: location.coords?.heading ?? null,
73
+ speed: location.coords?.speed ?? null
74
+ },
75
+ timestamp: location.timestamp ?? null
76
+ });
77
+
78
+ /**
79
+ * Adapts expo-location to the library's geolocation seam.
80
+ *
81
+ * The interesting half is `watchPosition`. The seam must hand back a numeric
82
+ * watch id SYNCHRONOUSLY, while `watchPositionAsync` only resolves its
83
+ * subscription later. So the adapter mints its own id immediately and stores
84
+ * the subscription against it on arrival. `clearWatch` called during that
85
+ * window records the id as cancelled, and the subscription is removed the
86
+ * moment it lands — without that, a screen that unmounts before the watch
87
+ * starts leaks a live GPS listener for the rest of the session, which is a
88
+ * battery drain nobody would attribute to this code.
89
+ */
90
+ export const adaptExpoLocation = mod => {
91
+ const subscriptions = new Map();
92
+ let nextWatchId = 1;
93
+ return {
94
+ getCurrentPosition: (onSuccess, onError, options) => {
95
+ mod.getCurrentPositionAsync(toExpoOptions(options)).then(location => onSuccess(toNativePosition(location))).catch(error => onError?.(toNativeError(error)));
96
+ },
97
+ watchPosition: (onSuccess, onError, options) => {
98
+ const watchId = nextWatchId++;
99
+ subscriptions.set(watchId, null);
100
+ mod.watchPositionAsync(toExpoOptions(options), location => onSuccess(toNativePosition(location))).then(subscription => {
101
+ if (!subscriptions.has(watchId)) {
102
+ // clearWatch already ran. Remove immediately rather than storing a
103
+ // subscription nothing will ever clear.
104
+ subscription.remove();
105
+ return;
106
+ }
107
+ subscriptions.set(watchId, subscription);
108
+ }).catch(error => {
109
+ subscriptions.delete(watchId);
110
+ onError?.(toNativeError(error));
111
+ });
112
+ return watchId;
113
+ },
114
+ clearWatch: watchId => {
115
+ const subscription = subscriptions.get(watchId);
116
+ subscriptions.delete(watchId);
117
+ subscription?.remove();
118
+ }
119
+ };
120
+ };
121
+ //# sourceMappingURL=expoLocation.js.map