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