@webority-technologies/mobile-core 0.0.1 → 0.0.3
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.
- package/README.md +22 -1
- package/lib/commonjs/api/baseUrl.js +34 -0
- package/lib/commonjs/api/curl.js +109 -0
- package/lib/commonjs/api/publicClient.js +11 -20
- package/lib/commonjs/api/retry.js +75 -0
- package/lib/commonjs/api/userClient.js +46 -22
- package/lib/commonjs/auth/authStore.js +101 -33
- package/lib/commonjs/auth/jwt.js +32 -8
- package/lib/commonjs/config/index.js +2 -1
- package/lib/commonjs/deepLink/index.js +286 -0
- package/lib/commonjs/download/adapters/expoFs.js +100 -0
- package/lib/commonjs/download/adapters/shared.js +13 -0
- package/lib/commonjs/download/index.js +272 -0
- package/lib/commonjs/formatters/date.js +1 -1
- package/lib/commonjs/formatters/index.js +24 -0
- package/lib/commonjs/formatters/phone.js +132 -15
- package/lib/commonjs/geolocation/adapters/expoLocation.js +126 -0
- package/lib/commonjs/geolocation/index.js +339 -0
- package/lib/commonjs/index.js +151 -48
- package/lib/commonjs/initSDK.js +23 -2
- package/lib/commonjs/initUserAuth.js +2 -1
- package/lib/commonjs/logger/index.js +159 -5
- package/lib/commonjs/network/index.js +6 -0
- package/lib/commonjs/network/networkStatus.js +50 -15
- package/lib/commonjs/permissions/index.js +24 -1
- package/lib/commonjs/sqlite/adapters/expo.js +53 -0
- package/lib/commonjs/sqlite/adapters/op.js +43 -0
- package/lib/commonjs/sqlite/adapters/rows.js +33 -0
- package/lib/commonjs/sqlite/adapters/storage.js +50 -0
- package/lib/commonjs/sqlite/adapters/sync.js +45 -0
- package/lib/commonjs/sqlite/index.js +610 -0
- package/lib/commonjs/sqlite/nitro.js +17 -0
- package/lib/commonjs/sqlite/op.js +18 -0
- package/lib/commonjs/sqlite/quick.js +14 -0
- package/lib/commonjs/sqlite/storage.js +14 -0
- package/lib/commonjs/storage/index.js +41 -20
- package/lib/commonjs/types/globals.d.js +6 -0
- package/lib/commonjs/utils/imageCompression.js +51 -14
- package/lib/commonjs/utils/index.js +6 -0
- package/lib/commonjs/versionCheck/index.js +28 -4
- package/lib/module/api/baseUrl.js +30 -0
- package/lib/module/api/curl.js +104 -0
- package/lib/module/api/publicClient.js +11 -20
- package/lib/module/api/retry.js +69 -0
- package/lib/module/api/userClient.js +45 -21
- package/lib/module/auth/authStore.js +99 -32
- package/lib/module/auth/jwt.js +32 -8
- package/lib/module/config/index.js +2 -1
- package/lib/module/deepLink/index.js +280 -0
- package/lib/module/download/adapters/expoFs.js +95 -0
- package/lib/module/download/adapters/shared.js +8 -0
- package/lib/module/download/index.js +264 -0
- package/lib/module/formatters/date.js +1 -1
- package/lib/module/formatters/index.js +1 -1
- package/lib/module/formatters/phone.js +110 -13
- package/lib/module/geolocation/adapters/expoLocation.js +121 -0
- package/lib/module/geolocation/index.js +332 -0
- package/lib/module/index.js +28 -11
- package/lib/module/initSDK.js +23 -2
- package/lib/module/initUserAuth.js +2 -1
- package/lib/module/logger/index.js +156 -4
- package/lib/module/network/index.js +1 -1
- package/lib/module/network/networkStatus.js +47 -14
- package/lib/module/permissions/index.js +22 -0
- package/lib/module/sqlite/adapters/expo.js +48 -0
- package/lib/module/sqlite/adapters/op.js +38 -0
- package/lib/module/sqlite/adapters/rows.js +28 -0
- package/lib/module/sqlite/adapters/storage.js +46 -0
- package/lib/module/sqlite/adapters/sync.js +41 -0
- package/lib/module/sqlite/index.js +600 -0
- package/lib/module/sqlite/nitro.js +12 -0
- package/lib/module/sqlite/op.js +13 -0
- package/lib/module/sqlite/quick.js +9 -0
- package/lib/module/sqlite/storage.js +9 -0
- package/lib/module/storage/index.js +38 -19
- package/lib/module/types/globals.d.js +10 -0
- package/lib/module/utils/imageCompression.js +49 -13
- package/lib/module/utils/index.js +1 -1
- package/lib/module/versionCheck/index.js +27 -4
- package/lib/typescript/commonjs/api/baseUrl.d.ts +20 -0
- package/lib/typescript/commonjs/api/curl.d.ts +18 -0
- package/lib/typescript/commonjs/api/retry.d.ts +27 -0
- package/lib/typescript/commonjs/api/userClient.d.ts +11 -0
- package/lib/typescript/commonjs/auth/authStore.d.ts +29 -6
- package/lib/typescript/commonjs/config/index.d.ts +6 -0
- package/lib/typescript/commonjs/deepLink/index.d.ts +54 -0
- package/lib/typescript/commonjs/download/adapters/expoFs.d.ts +45 -0
- package/lib/typescript/commonjs/download/adapters/shared.d.ts +3 -0
- package/lib/typescript/commonjs/download/index.d.ts +76 -0
- package/lib/typescript/commonjs/formatters/index.d.ts +2 -1
- package/lib/typescript/commonjs/formatters/phone.d.ts +27 -1
- package/lib/typescript/commonjs/geolocation/adapters/expoLocation.d.ts +42 -0
- package/lib/typescript/commonjs/geolocation/index.d.ts +89 -0
- package/lib/typescript/commonjs/index.d.ts +30 -13
- package/lib/typescript/commonjs/initSDK.d.ts +32 -0
- package/lib/typescript/commonjs/logger/index.d.ts +39 -0
- package/lib/typescript/commonjs/network/index.d.ts +2 -2
- package/lib/typescript/commonjs/network/networkStatus.d.ts +16 -0
- package/lib/typescript/commonjs/permissions/index.d.ts +35 -0
- package/lib/typescript/commonjs/sqlite/adapters/expo.d.ts +30 -0
- package/lib/typescript/commonjs/sqlite/adapters/op.d.ts +27 -0
- package/lib/typescript/commonjs/sqlite/adapters/rows.d.ts +7 -0
- package/lib/typescript/commonjs/sqlite/adapters/storage.d.ts +30 -0
- package/lib/typescript/commonjs/sqlite/adapters/sync.d.ts +31 -0
- package/lib/typescript/commonjs/sqlite/index.d.ts +121 -0
- package/lib/typescript/commonjs/sqlite/nitro.d.ts +7 -0
- package/lib/typescript/commonjs/sqlite/op.d.ts +8 -0
- package/lib/typescript/commonjs/sqlite/quick.d.ts +4 -0
- package/lib/typescript/commonjs/sqlite/storage.d.ts +4 -0
- package/lib/typescript/commonjs/storage/index.d.ts +25 -3
- package/lib/typescript/commonjs/utils/imageCompression.d.ts +17 -0
- package/lib/typescript/commonjs/utils/index.d.ts +2 -2
- package/lib/typescript/module/api/baseUrl.d.ts +20 -0
- package/lib/typescript/module/api/curl.d.ts +18 -0
- package/lib/typescript/module/api/retry.d.ts +27 -0
- package/lib/typescript/module/api/userClient.d.ts +11 -0
- package/lib/typescript/module/auth/authStore.d.ts +29 -6
- package/lib/typescript/module/config/index.d.ts +6 -0
- package/lib/typescript/module/deepLink/index.d.ts +54 -0
- package/lib/typescript/module/download/adapters/expoFs.d.ts +45 -0
- package/lib/typescript/module/download/adapters/shared.d.ts +3 -0
- package/lib/typescript/module/download/index.d.ts +76 -0
- package/lib/typescript/module/formatters/index.d.ts +2 -1
- package/lib/typescript/module/formatters/phone.d.ts +27 -1
- package/lib/typescript/module/geolocation/adapters/expoLocation.d.ts +42 -0
- package/lib/typescript/module/geolocation/index.d.ts +89 -0
- package/lib/typescript/module/index.d.ts +30 -13
- package/lib/typescript/module/initSDK.d.ts +32 -0
- package/lib/typescript/module/logger/index.d.ts +39 -0
- package/lib/typescript/module/network/index.d.ts +2 -2
- package/lib/typescript/module/network/networkStatus.d.ts +16 -0
- package/lib/typescript/module/permissions/index.d.ts +35 -0
- package/lib/typescript/module/sqlite/adapters/expo.d.ts +30 -0
- package/lib/typescript/module/sqlite/adapters/op.d.ts +27 -0
- package/lib/typescript/module/sqlite/adapters/rows.d.ts +7 -0
- package/lib/typescript/module/sqlite/adapters/storage.d.ts +30 -0
- package/lib/typescript/module/sqlite/adapters/sync.d.ts +31 -0
- package/lib/typescript/module/sqlite/index.d.ts +121 -0
- package/lib/typescript/module/sqlite/nitro.d.ts +7 -0
- package/lib/typescript/module/sqlite/op.d.ts +8 -0
- package/lib/typescript/module/sqlite/quick.d.ts +4 -0
- package/lib/typescript/module/sqlite/storage.d.ts +4 -0
- package/lib/typescript/module/storage/index.d.ts +25 -3
- package/lib/typescript/module/utils/imageCompression.d.ts +17 -0
- package/lib/typescript/module/utils/index.d.ts +2 -2
- package/package.json +102 -13
|
@@ -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, isValidPhoneNumber, normalizePhone, parsePhoneNumber, parsePhoneNumberFromString } from "./phone.js";
|
|
8
8
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Real cross-country validation, re-exported rather than reimplemented.
|
|
5
|
+
* formatPhone/normalizePhone/detectPhoneCountry below are a fast, dependency-
|
|
6
|
+
* light DISPLAY formatter for a fixed set of countries; they are not a
|
|
7
|
+
* validator. Real phone number plans have country-specific area-code and
|
|
8
|
+
* mobile-prefix rules that only a maintained metadata table gets right -
|
|
9
|
+
* hand-rolling that risks silently-wrong validation for a country nobody
|
|
10
|
+
* tests against. Both live here so consumers get one phone module, not two.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { isValidPhoneNumber, parsePhoneNumber, parsePhoneNumberFromString } from 'libphonenumber-js';
|
|
3
14
|
/**
|
|
4
15
|
* Strip everything except digits and a leading `+` (preserved if present).
|
|
5
16
|
*/
|
|
@@ -20,14 +31,90 @@ const groupIndianMobile = digits => {
|
|
|
20
31
|
return `${digits.slice(0, 5)} ${digits.slice(5)}`;
|
|
21
32
|
};
|
|
22
33
|
|
|
34
|
+
// Longest calling code first so a 3-digit code is never shadowed by a shorter one.
|
|
35
|
+
const COUNTRY_RULES = [{
|
|
36
|
+
country: 'AE',
|
|
37
|
+
callingCode: '971',
|
|
38
|
+
nationalLength: 9,
|
|
39
|
+
group: n => `${n.slice(0, 2)} ${n.slice(2, 5)} ${n.slice(5)}`
|
|
40
|
+
}, {
|
|
41
|
+
country: 'IN',
|
|
42
|
+
callingCode: '91',
|
|
43
|
+
nationalLength: 10,
|
|
44
|
+
group: groupIndianMobile
|
|
45
|
+
}, {
|
|
46
|
+
country: 'GB',
|
|
47
|
+
callingCode: '44',
|
|
48
|
+
nationalLength: 10,
|
|
49
|
+
group: n => `${n.slice(0, 4)} ${n.slice(4, 7)} ${n.slice(7)}`
|
|
50
|
+
}, {
|
|
51
|
+
country: 'AU',
|
|
52
|
+
callingCode: '61',
|
|
53
|
+
nationalLength: 9,
|
|
54
|
+
group: n => `${n.slice(0, 3)} ${n.slice(3, 6)} ${n.slice(6)}`
|
|
55
|
+
}, {
|
|
56
|
+
country: 'SG',
|
|
57
|
+
callingCode: '65',
|
|
58
|
+
nationalLength: 8,
|
|
59
|
+
group: n => `${n.slice(0, 4)} ${n.slice(4)}`
|
|
60
|
+
}, {
|
|
61
|
+
country: 'US',
|
|
62
|
+
callingCode: '1',
|
|
63
|
+
nationalLength: 10,
|
|
64
|
+
group: n => `(${n.slice(0, 3)}) ${n.slice(3, 6)}-${n.slice(6)}`
|
|
65
|
+
},
|
|
66
|
+
// US and CA share a calling code and are indistinguishable from it alone;
|
|
67
|
+
// this row only serves an explicit `country: 'CA'` override, never detection
|
|
68
|
+
// (it sits after the US row, and findRuleByCallingCode returns the first match).
|
|
69
|
+
{
|
|
70
|
+
country: 'CA',
|
|
71
|
+
callingCode: '1',
|
|
72
|
+
nationalLength: 10,
|
|
73
|
+
group: n => `(${n.slice(0, 3)}) ${n.slice(3, 6)}-${n.slice(6)}`
|
|
74
|
+
}];
|
|
75
|
+
const findRuleByCallingCode = rest => COUNTRY_RULES.find(rule => rest.startsWith(rule.callingCode));
|
|
76
|
+
const findRuleByCountry = country => COUNTRY_RULES.find(rule => rule.country === country);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Detect the country of a phone number: from its own `+` country code when
|
|
80
|
+
* present, otherwise from `defaultCountryCode`. Returns null when neither
|
|
81
|
+
* carries a recognised calling code.
|
|
82
|
+
*/
|
|
83
|
+
export const detectPhoneCountry = (input, defaultCountryCode = '+91') => {
|
|
84
|
+
const normalized = normalizePhone(input);
|
|
85
|
+
if (!normalized) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (normalized.startsWith('+')) {
|
|
89
|
+
return findRuleByCallingCode(normalized.slice(1))?.country ?? null;
|
|
90
|
+
}
|
|
91
|
+
const normalizedDefault = normalizePhone(defaultCountryCode);
|
|
92
|
+
if (!normalizedDefault.startsWith('+')) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return findRuleByCallingCode(normalizedDefault.slice(1))?.country ?? null;
|
|
96
|
+
};
|
|
97
|
+
const bestEffortGroup = rest => {
|
|
98
|
+
// No known country code matched (or matched but the wrong length): split
|
|
99
|
+
// the country code (first 1-3 digits) and group the rest in 4s.
|
|
100
|
+
const ccLen = rest.length > 11 ? 3 : rest.length > 10 ? 2 : 1;
|
|
101
|
+
const cc = rest.slice(0, ccLen);
|
|
102
|
+
const number = rest.slice(ccLen);
|
|
103
|
+
const grouped = number.replace(/(.{4})(?=.)/g, '$1 ').trim();
|
|
104
|
+
return `+${cc} ${grouped}`.trim();
|
|
105
|
+
};
|
|
106
|
+
|
|
23
107
|
/**
|
|
24
108
|
* Format a phone number for human display.
|
|
25
109
|
*
|
|
26
110
|
* - Indian mobile (10 digits) → `XXXXX XXXXX`
|
|
27
111
|
* - With `+91` country code → `+91 XXXXX XXXXX`
|
|
112
|
+
* - A number that already carries a recognised country code is grouped for
|
|
113
|
+
* that country regardless of `defaultCountryCode`.
|
|
28
114
|
* - Other inputs are returned as `+CC NNNN NNNN…` best-effort grouping.
|
|
29
115
|
*/
|
|
30
|
-
|
|
116
|
+
|
|
117
|
+
export function formatPhone(input, arg) {
|
|
31
118
|
if (!input) {
|
|
32
119
|
return '';
|
|
33
120
|
}
|
|
@@ -35,20 +122,30 @@ export const formatPhone = (input, defaultCountryCode = '+91') => {
|
|
|
35
122
|
if (!normalized) {
|
|
36
123
|
return '';
|
|
37
124
|
}
|
|
38
|
-
|
|
39
|
-
|
|
125
|
+
const options = typeof arg === 'string' ? {} : arg ?? {};
|
|
126
|
+
const defaultCountryCode = typeof arg === 'string' ? arg : options.defaultCountryCode ?? '+91';
|
|
40
127
|
if (normalized.startsWith('+')) {
|
|
41
128
|
const rest = normalized.slice(1);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
129
|
+
const detectedRule = findRuleByCallingCode(rest);
|
|
130
|
+
const activeRule = options.country ? findRuleByCountry(options.country) : detectedRule;
|
|
131
|
+
if (activeRule) {
|
|
132
|
+
const national = detectedRule ? rest.slice(detectedRule.callingCode.length) : rest;
|
|
133
|
+
if (national.length === activeRule.nationalLength) {
|
|
134
|
+
const includeCc = options.includeCountryCode ?? true;
|
|
135
|
+
const body = activeRule.group(national);
|
|
136
|
+
return includeCc ? `+${activeRule.callingCode} ${body}` : body;
|
|
137
|
+
}
|
|
45
138
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
139
|
+
return bestEffortGroup(rest);
|
|
140
|
+
}
|
|
141
|
+
if (options.country) {
|
|
142
|
+
const rule = findRuleByCountry(options.country);
|
|
143
|
+
if (rule && normalized.length === rule.nationalLength) {
|
|
144
|
+
const includeCc = options.includeCountryCode ?? false;
|
|
145
|
+
const body = rule.group(normalized);
|
|
146
|
+
return includeCc ? `+${rule.callingCode} ${body}` : body;
|
|
147
|
+
}
|
|
148
|
+
return normalized.replace(/(.{4})(?=.)/g, '$1 ').trim();
|
|
52
149
|
}
|
|
53
150
|
|
|
54
151
|
// No country code; assume default if length matches Indian mobile.
|
|
@@ -57,5 +154,5 @@ export const formatPhone = (input, defaultCountryCode = '+91') => {
|
|
|
57
154
|
}
|
|
58
155
|
// Otherwise just return groups of 4.
|
|
59
156
|
return normalized.replace(/(.{4})(?=.)/g, '$1 ').trim();
|
|
60
|
-
}
|
|
157
|
+
}
|
|
61
158
|
//# 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
|