@webority/mobile-core 0.0.28 → 0.1.0
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/lib/commonjs/api/baseUrl.js +1 -34
- package/lib/commonjs/api/curl.js +1 -109
- package/lib/commonjs/api/multipart.js +1 -21
- package/lib/commonjs/api/publicClient.js +1 -79
- package/lib/commonjs/api/responseBodyText.js +1 -0
- package/lib/commonjs/api/retry.js +1 -91
- package/lib/commonjs/api/tokenRefresh.js +1 -205
- package/lib/commonjs/api/userClient.js +1 -265
- package/lib/commonjs/auth/authStore.js +1 -265
- package/lib/commonjs/auth/index.js +1 -44
- package/lib/commonjs/auth/jwt.js +1 -112
- package/lib/commonjs/config/index.js +1 -72
- package/lib/commonjs/deepLink/index.js +1 -286
- package/lib/commonjs/download/adapters/expoFs.js +1 -69
- package/lib/commonjs/download/adapters/shared.js +1 -13
- package/lib/commonjs/download/index.js +1 -250
- package/lib/commonjs/formatters/currency.js +1 -70
- package/lib/commonjs/formatters/date.js +1 -167
- package/lib/commonjs/formatters/dateMath.js +1 -311
- package/lib/commonjs/formatters/index.js +1 -246
- package/lib/commonjs/formatters/initials.js +1 -32
- package/lib/commonjs/formatters/intlCache.js +1 -37
- package/lib/commonjs/formatters/number.js +1 -89
- package/lib/commonjs/formatters/phone.js +1 -369
- package/lib/commonjs/formatters/windowsToIanaTable.js +1 -164
- package/lib/commonjs/geolocation/adapters/expoLocation.js +1 -126
- package/lib/commonjs/geolocation/index.js +1 -355
- package/lib/commonjs/index.js +1 -644
- package/lib/commonjs/initSDK.js +1 -66
- package/lib/commonjs/initUserAuth.js +1 -37
- package/lib/commonjs/logger/crashlytics.js +1 -0
- package/lib/commonjs/logger/index.js +1 -301
- package/lib/commonjs/logger/redact.js +1 -50
- package/lib/commonjs/network/index.js +1 -31
- package/lib/commonjs/network/networkStatus.js +1 -171
- package/lib/commonjs/permissions/calendar.js +1 -14
- package/lib/commonjs/permissions/camera.js +1 -14
- package/lib/commonjs/permissions/contacts.js +1 -10
- package/lib/commonjs/permissions/index.js +1 -133
- package/lib/commonjs/permissions/location.js +1 -14
- package/lib/commonjs/permissions/mediaLibrary.js +1 -10
- package/lib/commonjs/permissions/notifications.js +1 -10
- package/lib/commonjs/permissions/photos.js +1 -14
- package/lib/commonjs/phone/countries.js +1 -265
- package/lib/commonjs/push/index.js +1 -61
- package/lib/commonjs/push/push.js +1 -443
- package/lib/commonjs/push/status.js +1 -0
- package/lib/commonjs/push/task.js +1 -43
- package/lib/commonjs/secureScreen/index.js +1 -0
- package/lib/commonjs/share/index.js +1 -70
- package/lib/commonjs/smsRetriever/index.js +1 -91
- package/lib/commonjs/specs/NativeSmsRetriever.js +1 -15
- package/lib/commonjs/sqlite/adapters/expo.js +1 -53
- package/lib/commonjs/sqlite/index.js +1 -604
- package/lib/commonjs/storage/index.js +1 -214
- package/lib/commonjs/types/axios.js +1 -4
- package/lib/commonjs/types/globals.d.js +1 -6
- package/lib/commonjs/utils/base64.js +1 -24
- package/lib/commonjs/utils/imageCompression.js +1 -389
- package/lib/commonjs/utils/index.js +1 -19
- package/lib/commonjs/validators/index.js +1 -227
- package/lib/commonjs/versionCheck/index.js +1 -419
- package/lib/module/api/baseUrl.js +1 -30
- package/lib/module/api/curl.js +1 -104
- package/lib/module/api/multipart.js +1 -16
- package/lib/module/api/publicClient.js +1 -75
- package/lib/module/api/responseBodyText.js +1 -0
- package/lib/module/api/retry.js +1 -85
- package/lib/module/api/tokenRefresh.js +1 -192
- package/lib/module/api/userClient.js +1 -243
- package/lib/module/auth/authStore.js +1 -257
- package/lib/module/auth/index.js +1 -5
- package/lib/module/auth/jwt.js +1 -106
- package/lib/module/config/index.js +1 -64
- package/lib/module/deepLink/index.js +1 -280
- package/lib/module/download/adapters/expoFs.js +1 -64
- package/lib/module/download/adapters/shared.js +1 -8
- package/lib/module/download/index.js +1 -242
- package/lib/module/formatters/currency.js +1 -65
- package/lib/module/formatters/date.js +1 -159
- package/lib/module/formatters/dateMath.js +1 -295
- package/lib/module/formatters/index.js +1 -9
- package/lib/module/formatters/initials.js +1 -27
- package/lib/module/formatters/intlCache.js +1 -30
- package/lib/module/formatters/number.js +1 -82
- package/lib/module/formatters/phone.js +1 -302
- package/lib/module/formatters/windowsToIanaTable.js +1 -160
- package/lib/module/geolocation/adapters/expoLocation.js +1 -121
- package/lib/module/geolocation/index.js +1 -347
- package/lib/module/index.js +1 -57
- package/lib/module/initSDK.js +1 -61
- package/lib/module/initUserAuth.js +1 -33
- package/lib/module/logger/crashlytics.js +1 -0
- package/lib/module/logger/index.js +1 -295
- package/lib/module/logger/redact.js +1 -44
- package/lib/module/network/index.js +1 -4
- package/lib/module/network/networkStatus.js +1 -161
- package/lib/module/permissions/calendar.js +1 -15
- package/lib/module/permissions/camera.js +1 -15
- package/lib/module/permissions/contacts.js +1 -11
- package/lib/module/permissions/index.js +1 -128
- package/lib/module/permissions/location.js +1 -15
- package/lib/module/permissions/mediaLibrary.js +1 -11
- package/lib/module/permissions/notifications.js +1 -11
- package/lib/module/permissions/photos.js +1 -15
- package/lib/module/phone/countries.js +1 -261
- package/lib/module/push/index.js +1 -4
- package/lib/module/push/push.js +1 -427
- package/lib/module/push/status.js +1 -0
- package/lib/module/push/task.js +1 -38
- package/lib/module/secureScreen/index.js +1 -0
- package/lib/module/share/index.js +1 -64
- package/lib/module/smsRetriever/index.js +1 -86
- package/lib/module/specs/NativeSmsRetriever.js +1 -13
- package/lib/module/sqlite/adapters/expo.js +1 -48
- package/lib/module/sqlite/index.js +1 -594
- package/lib/module/storage/index.js +1 -208
- package/lib/module/types/axios.js +1 -4
- package/lib/module/types/globals.d.js +1 -10
- package/lib/module/utils/base64.js +1 -19
- package/lib/module/utils/imageCompression.js +1 -383
- package/lib/module/utils/index.js +1 -4
- package/lib/module/validators/index.js +1 -205
- package/lib/module/versionCheck/index.js +1 -413
- package/lib/typescript/commonjs/api/responseBodyText.d.ts +6 -0
- package/lib/typescript/commonjs/index.d.ts +5 -0
- package/lib/typescript/commonjs/logger/crashlytics.d.ts +31 -0
- package/lib/typescript/commonjs/push/index.d.ts +3 -1
- package/lib/typescript/commonjs/push/push.d.ts +9 -1
- package/lib/typescript/commonjs/push/status.d.ts +9 -0
- package/lib/typescript/commonjs/secureScreen/index.d.ts +25 -0
- package/lib/typescript/module/api/responseBodyText.d.ts +6 -0
- package/lib/typescript/module/index.d.ts +5 -0
- package/lib/typescript/module/logger/crashlytics.d.ts +31 -0
- package/lib/typescript/module/push/index.d.ts +3 -1
- package/lib/typescript/module/push/push.d.ts +9 -1
- package/lib/typescript/module/push/status.d.ts +9 -0
- package/lib/typescript/module/secureScreen/index.d.ts +25 -0
- package/package.json +23 -4
|
@@ -1,64 +1 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Configuration management for Webority packages
|
|
5
|
-
* Provides a flexible way to inject configuration without hard dependencies
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Info lines (each token refresh, each unauthorised URL) reach the device log
|
|
10
|
-
* and any remote sink, so a release build that sets nothing logs warnings and
|
|
11
|
-
* errors only.
|
|
12
|
-
*/
|
|
13
|
-
export const DEFAULT_LOG_LEVEL = __DEV__ ? 'info' : 'warn';
|
|
14
|
-
const DEFAULT_CONFIG = {
|
|
15
|
-
baseUrl: null,
|
|
16
|
-
loggingEnabled: true,
|
|
17
|
-
logLevel: DEFAULT_LOG_LEVEL,
|
|
18
|
-
environment: 'Development',
|
|
19
|
-
country: 'in',
|
|
20
|
-
appId: null,
|
|
21
|
-
appVersion: null,
|
|
22
|
-
logCurl: false
|
|
23
|
-
};
|
|
24
|
-
let globalConfig = {
|
|
25
|
-
...DEFAULT_CONFIG
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Set global configuration for all Webority packages
|
|
30
|
-
*/
|
|
31
|
-
export const setConfig = config => {
|
|
32
|
-
globalConfig = {
|
|
33
|
-
...globalConfig,
|
|
34
|
-
...config
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Get current configuration
|
|
40
|
-
*/
|
|
41
|
-
export const getConfig = () => globalConfig;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Get a specific config value with optional fallback.
|
|
45
|
-
* If the stored value is `null`, the fallback is returned (when provided).
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
export function getConfigValue(key, fallback) {
|
|
49
|
-
const value = globalConfig[key];
|
|
50
|
-
if (value !== null) {
|
|
51
|
-
return value;
|
|
52
|
-
}
|
|
53
|
-
return fallback;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Reset configuration to defaults
|
|
58
|
-
*/
|
|
59
|
-
export const resetConfig = () => {
|
|
60
|
-
globalConfig = {
|
|
61
|
-
...DEFAULT_CONFIG
|
|
62
|
-
};
|
|
63
|
-
};
|
|
64
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
export const DEFAULT_LOG_LEVEL=__DEV__?"info":"warn";const n={baseUrl:null,loggingEnabled:!0,logLevel:DEFAULT_LOG_LEVEL,environment:"Development",country:"in",appId:null,appVersion:null,logCurl:!1};let o={...n};export const setConfig=n=>{o={...o,...n}};export const getConfig=()=>o;export function getConfigValue(n,e){const t=o[n];return null!==t?t:e}export const resetConfig=()=>{o={...n}};
|
|
@@ -1,280 +1 @@
|
|
|
1
|
-
"use
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
4
|
-
import { Linking } from 'react-native';
|
|
5
|
-
import { Logger } from "../logger/index.js";
|
|
6
|
-
const PREFIX = '[@webority/mobile-core]';
|
|
7
|
-
const decodeSafe = value => {
|
|
8
|
-
try {
|
|
9
|
-
return decodeURIComponent(value.replace(/\+/g, ' '));
|
|
10
|
-
} catch {
|
|
11
|
-
return value;
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
const parseQuery = raw => {
|
|
15
|
-
const out = {};
|
|
16
|
-
if (!raw) {
|
|
17
|
-
return out;
|
|
18
|
-
}
|
|
19
|
-
for (const pair of raw.split('&')) {
|
|
20
|
-
if (!pair) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const eq = pair.indexOf('=');
|
|
24
|
-
const key = decodeSafe(eq >= 0 ? pair.slice(0, eq) : pair);
|
|
25
|
-
if (!key) {
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
out[key] = eq >= 0 ? decodeSafe(pair.slice(eq + 1)) : '';
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
31
|
-
};
|
|
32
|
-
const trimSlashes = value => value.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Parse a deep-link URL. Returns null for anything that is not a usable URL so
|
|
36
|
-
* a hostile or truncated link can never throw on the native event path.
|
|
37
|
-
*/
|
|
38
|
-
export const parseDeepLink = url => {
|
|
39
|
-
if (typeof url !== 'string') {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
const trimmed = url.trim();
|
|
43
|
-
if (!trimmed || /\s/.test(trimmed)) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
const withoutFragment = trimmed.split('#')[0] ?? '';
|
|
47
|
-
const schemeEnd = withoutFragment.indexOf('://');
|
|
48
|
-
let scheme = '';
|
|
49
|
-
let rest = withoutFragment;
|
|
50
|
-
if (schemeEnd >= 0) {
|
|
51
|
-
const rawScheme = withoutFragment.slice(0, schemeEnd);
|
|
52
|
-
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*$/.test(rawScheme)) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
scheme = rawScheme.toLowerCase();
|
|
56
|
-
rest = withoutFragment.slice(schemeEnd + 3);
|
|
57
|
-
}
|
|
58
|
-
const queryStart = rest.indexOf('?');
|
|
59
|
-
const rawPath = queryStart >= 0 ? rest.slice(0, queryStart) : rest;
|
|
60
|
-
const query = parseQuery(queryStart >= 0 ? rest.slice(queryStart + 1) : '');
|
|
61
|
-
let host = '';
|
|
62
|
-
let pathPart = rawPath;
|
|
63
|
-
if (scheme === 'http' || scheme === 'https') {
|
|
64
|
-
const slash = rawPath.indexOf('/');
|
|
65
|
-
host = slash >= 0 ? rawPath.slice(0, slash) : rawPath;
|
|
66
|
-
pathPart = slash >= 0 ? rawPath.slice(slash + 1) : '';
|
|
67
|
-
}
|
|
68
|
-
const path = trimSlashes(pathPart);
|
|
69
|
-
const segments = path ? path.split('/').filter(Boolean) : [];
|
|
70
|
-
if (scheme && scheme !== 'http' && scheme !== 'https') {
|
|
71
|
-
host = segments[0] ?? '';
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
url: trimmed,
|
|
75
|
-
scheme,
|
|
76
|
-
host,
|
|
77
|
-
path,
|
|
78
|
-
segments,
|
|
79
|
-
query
|
|
80
|
-
};
|
|
81
|
-
};
|
|
82
|
-
const matchPattern = (pattern, segments) => {
|
|
83
|
-
const patternSegments = trimSlashes(pattern).split('/').filter(Boolean);
|
|
84
|
-
if (patternSegments.length !== segments.length) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
const params = {};
|
|
88
|
-
for (let i = 0; i < patternSegments.length; i += 1) {
|
|
89
|
-
const p = patternSegments[i];
|
|
90
|
-
const s = segments[i];
|
|
91
|
-
if (p.startsWith(':')) {
|
|
92
|
-
const name = p.slice(1);
|
|
93
|
-
if (!name) {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
params[name] = s;
|
|
97
|
-
} else if (p.toLowerCase() !== s.toLowerCase()) {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return params;
|
|
102
|
-
};
|
|
103
|
-
const matchRegExp = (pattern, path) => {
|
|
104
|
-
// A /g or /y pattern carries lastIndex between calls, so the same link would
|
|
105
|
-
// match on one dispatch and miss on the next.
|
|
106
|
-
if (pattern.global || pattern.sticky) {
|
|
107
|
-
pattern.lastIndex = 0;
|
|
108
|
-
}
|
|
109
|
-
const result = pattern.exec(path);
|
|
110
|
-
if (!result) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
const params = {};
|
|
114
|
-
for (let i = 1; i < result.length; i += 1) {
|
|
115
|
-
const group = result[i];
|
|
116
|
-
if (group !== undefined) {
|
|
117
|
-
params[String(i)] = group;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
for (const [name, value] of Object.entries(result.groups ?? {})) {
|
|
121
|
-
if (value !== undefined) {
|
|
122
|
-
params[name] = value;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return params;
|
|
126
|
-
};
|
|
127
|
-
const matchRoute = (routes, link) => {
|
|
128
|
-
for (const route of routes) {
|
|
129
|
-
const params = typeof route.match === 'string' ? matchPattern(route.match, link.segments) : matchRegExp(route.match, link.path);
|
|
130
|
-
if (params) {
|
|
131
|
-
return {
|
|
132
|
-
route,
|
|
133
|
-
params
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return null;
|
|
138
|
-
};
|
|
139
|
-
let config = null;
|
|
140
|
-
let subscription = null;
|
|
141
|
-
let started = false;
|
|
142
|
-
let pending = null;
|
|
143
|
-
let readyOverride = null;
|
|
144
|
-
const isReady = () => {
|
|
145
|
-
if (readyOverride !== null) {
|
|
146
|
-
return readyOverride;
|
|
147
|
-
}
|
|
148
|
-
return config?.isReady?.() ?? true;
|
|
149
|
-
};
|
|
150
|
-
const isAuthenticated = () => config?.isAuthenticated?.() ?? true;
|
|
151
|
-
const dispatch = link => {
|
|
152
|
-
const active = config;
|
|
153
|
-
if (!active) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const matched = matchRoute(active.routes, link);
|
|
157
|
-
if (!matched) {
|
|
158
|
-
active.onUnmatched?.(link);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
if (matched.route.requiresAuth && !isAuthenticated()) {
|
|
162
|
-
pending = link;
|
|
163
|
-
active.onUnauthenticated?.(link);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
matched.route.handler(link, matched.params);
|
|
167
|
-
};
|
|
168
|
-
const flushPending = () => {
|
|
169
|
-
const link = pending;
|
|
170
|
-
if (!link) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
// Cleared BEFORE dispatch: a handler that navigates can re-enter this path,
|
|
174
|
-
// and a link that fires twice is the classic deep-link duplicate-screen bug.
|
|
175
|
-
pending = null;
|
|
176
|
-
dispatch(link);
|
|
177
|
-
};
|
|
178
|
-
const configure = next => {
|
|
179
|
-
if (!next || !Array.isArray(next.routes)) {
|
|
180
|
-
throw new Error(`${PREFIX} DeepLink.configure() needs a config object with a "routes" array. ` + 'Pass { routes: [{ match: "order/:id", handler }] }.');
|
|
181
|
-
}
|
|
182
|
-
config = next;
|
|
183
|
-
};
|
|
184
|
-
const handleUrl = url => {
|
|
185
|
-
if (!config) {
|
|
186
|
-
Logger.error(`${PREFIX} DeepLink received a URL before configure() was called; the link was dropped. ` + 'Call DeepLink.configure({ routes }) (or useDeepLink) during app start.');
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const parsed = parseDeepLink(url);
|
|
190
|
-
// A malformed URL still reaches the app through onUnmatched, with the raw
|
|
191
|
-
// value in `url`, rather than vanishing.
|
|
192
|
-
if (!parsed) {
|
|
193
|
-
config.onUnmatched?.({
|
|
194
|
-
url: typeof url === 'string' ? url : '',
|
|
195
|
-
scheme: '',
|
|
196
|
-
host: '',
|
|
197
|
-
path: '',
|
|
198
|
-
segments: [],
|
|
199
|
-
query: {}
|
|
200
|
-
});
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const link = parsed;
|
|
204
|
-
if (!isReady()) {
|
|
205
|
-
pending = link;
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
dispatch(link);
|
|
209
|
-
};
|
|
210
|
-
const start = async () => {
|
|
211
|
-
if (!config) {
|
|
212
|
-
throw new Error(`${PREFIX} DeepLink.start() was called before configure(). ` + 'Call DeepLink.configure({ routes }) first, or use the useDeepLink() hook.');
|
|
213
|
-
}
|
|
214
|
-
if (started) {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
started = true;
|
|
218
|
-
// Subscribe before reading the cold-start URL so a link arriving during the
|
|
219
|
-
// await is not lost.
|
|
220
|
-
subscription = Linking.addEventListener('url', event => {
|
|
221
|
-
handleUrl(event.url);
|
|
222
|
-
});
|
|
223
|
-
try {
|
|
224
|
-
const initial = await Linking.getInitialURL();
|
|
225
|
-
if (initial) {
|
|
226
|
-
handleUrl(initial);
|
|
227
|
-
}
|
|
228
|
-
} catch (error) {
|
|
229
|
-
Logger.error(`${PREFIX} DeepLink could not read the cold-start URL`, error);
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
const stop = () => {
|
|
233
|
-
subscription?.remove?.();
|
|
234
|
-
subscription = null;
|
|
235
|
-
started = false;
|
|
236
|
-
pending = null;
|
|
237
|
-
readyOverride = null;
|
|
238
|
-
};
|
|
239
|
-
const setReady = ready => {
|
|
240
|
-
readyOverride = ready;
|
|
241
|
-
if (ready) {
|
|
242
|
-
flushPending();
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
export const DeepLink = {
|
|
246
|
-
configure,
|
|
247
|
-
start,
|
|
248
|
-
stop,
|
|
249
|
-
handleUrl,
|
|
250
|
-
setReady,
|
|
251
|
-
/** Replay the stashed link, once. Call after a successful login. */
|
|
252
|
-
replayPending: flushPending,
|
|
253
|
-
getPending: () => pending
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Configure and start the deep-link service for the lifetime of a component.
|
|
258
|
-
* The stored config delegates to the latest render, so new route closures do
|
|
259
|
-
* not tear down and re-subscribe the native listener.
|
|
260
|
-
*/
|
|
261
|
-
export const useDeepLink = deepLinkConfig => {
|
|
262
|
-
const configRef = useRef(deepLinkConfig);
|
|
263
|
-
configRef.current = deepLinkConfig;
|
|
264
|
-
useEffect(() => {
|
|
265
|
-
DeepLink.configure({
|
|
266
|
-
get routes() {
|
|
267
|
-
return configRef.current.routes;
|
|
268
|
-
},
|
|
269
|
-
isReady: () => configRef.current.isReady?.() ?? true,
|
|
270
|
-
isAuthenticated: () => configRef.current.isAuthenticated?.() ?? true,
|
|
271
|
-
onUnauthenticated: link => configRef.current.onUnauthenticated?.(link),
|
|
272
|
-
onUnmatched: link => configRef.current.onUnmatched?.(link)
|
|
273
|
-
});
|
|
274
|
-
void DeepLink.start();
|
|
275
|
-
return () => {
|
|
276
|
-
DeepLink.stop();
|
|
277
|
-
};
|
|
278
|
-
}, []);
|
|
279
|
-
};
|
|
280
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import{useEffect as e,useRef as t}from"react";import{Linking as r}from"react-native";import{Logger as n}from"../logger/index.js";const o="[@webority/mobile-core]",decodeSafe=e=>{try{return decodeURIComponent(e.replace(/\+/g," "))}catch{return e}},trimSlashes=e=>e.replace(/^\/+/,"").replace(/\/+$/,"");export const parseDeepLink=e=>{if("string"!=typeof e)return null;const t=e.trim();if(!t||/\s/.test(t))return null;const r=t.split("#")[0]??"",n=r.indexOf("://");let o="",s=r;if(n>=0){const e=r.slice(0,n);if(!/^[a-zA-Z][a-zA-Z0-9+.-]*$/.test(e))return null;o=e.toLowerCase(),s=r.slice(n+3)}const i=s.indexOf("?"),c=i>=0?s.slice(0,i):s,l=(e=>{const t={};if(!e)return t;for(const r of e.split("&")){if(!r)continue;const e=r.indexOf("="),n=decodeSafe(e>=0?r.slice(0,e):r);n&&(t[n]=e>=0?decodeSafe(r.slice(e+1)):"")}return t})(i>=0?s.slice(i+1):"");let u="",a=c;if("http"===o||"https"===o){const e=c.indexOf("/");u=e>=0?c.slice(0,e):c,a=e>=0?c.slice(e+1):""}const f=trimSlashes(a),p=f?f.split("/").filter(Boolean):[];return o&&"http"!==o&&"https"!==o&&(u=p[0]??""),{url:t,scheme:o,host:u,path:f,segments:p,query:l}};const matchPattern=(e,t)=>{const r=trimSlashes(e).split("/").filter(Boolean);if(r.length!==t.length)return null;const n={};for(let e=0;e<r.length;e+=1){const o=r[e],s=t[e];if(o.startsWith(":")){const e=o.slice(1);if(!e)return null;n[e]=s}else if(o.toLowerCase()!==s.toLowerCase())return null}return n},matchRegExp=(e,t)=>{(e.global||e.sticky)&&(e.lastIndex=0);const r=e.exec(t);if(!r)return null;const n={};for(let e=1;e<r.length;e+=1){const t=r[e];void 0!==t&&(n[String(e)]=t)}for(const[e,t]of Object.entries(r.groups??{}))void 0!==t&&(n[e]=t);return n};let s=null,i=null,c=!1,l=null,u=null;const dispatch=e=>{const t=s;if(!t)return;const r=((e,t)=>{for(const r of e){const e="string"==typeof r.match?matchPattern(r.match,t.segments):matchRegExp(r.match,t.path);if(e)return{route:r,params:e}}return null})(t.routes,e);if(r)return r.route.requiresAuth&&!(s?.isAuthenticated?.()??1)?(l=e,void t.onUnauthenticated?.(e)):void r.route.handler(e,r.params);t.onUnmatched?.(e)},flushPending=()=>{const e=l;e&&(l=null,dispatch(e))},handleUrl=e=>{if(!s)return void n.error(`${o} DeepLink received a URL before configure() was called; the link was dropped. Call DeepLink.configure({ routes }) (or useDeepLink) during app start.`);const t=parseDeepLink(e);if(!t)return void s.onUnmatched?.({url:"string"==typeof e?e:"",scheme:"",host:"",path:"",segments:[],query:{}});const r=t;(null!==u?u:s?.isReady?.()??1)?dispatch(r):l=r};export const DeepLink={configure:e=>{if(!e||!Array.isArray(e.routes))throw new Error(`${o} DeepLink.configure() needs a config object with a "routes" array. Pass { routes: [{ match: "order/:id", handler }] }.`);s=e},start:async()=>{if(!s)throw new Error(`${o} DeepLink.start() was called before configure(). Call DeepLink.configure({ routes }) first, or use the useDeepLink() hook.`);if(!c){c=!0,i=r.addEventListener("url",e=>{handleUrl(e.url)});try{const e=await r.getInitialURL();e&&handleUrl(e)}catch(e){n.error(`${o} DeepLink could not read the cold-start URL`,e)}}},stop:()=>{i?.remove?.(),i=null,c=!1,l=null,u=null},handleUrl:handleUrl,setReady:e=>{u=e,e&&flushPending()},replayPending:flushPending,getPending:()=>l};export const useDeepLink=r=>{const n=t(r);n.current=r,e(()=>(DeepLink.configure({get routes(){return n.current.routes},isReady:()=>n.current.isReady?.()??!0,isAuthenticated:()=>n.current.isAuthenticated?.()??!0,onUnauthenticated:e=>n.current.onUnauthenticated?.(e),onUnmatched:e=>n.current.onUnmatched?.(e)}),DeepLink.start(),()=>{DeepLink.stop()}),[])};
|
|
@@ -1,64 +1 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* expo-file-system. Shape verified against 57.0.6, where the legacy function
|
|
5
|
-
* API (`downloadAsync`, `getInfoAsync`) still exists on the main entry point
|
|
6
|
-
* but every one of those functions THROWS at runtime; they now live behind
|
|
7
|
-
* `expo-file-system/legacy`. This adapter therefore uses the class API only.
|
|
8
|
-
*
|
|
9
|
-
* The REQUIRE lives in the entry point beside this file, never here.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* expo reports a directory as a URI with a trailing slash, and the download
|
|
14
|
-
* module joins a directory to a file name with a `/` of its own.
|
|
15
|
-
*/
|
|
16
|
-
const asDirectoryPath = uri => uri.replace(/\/+$/, '');
|
|
17
|
-
export const adaptExpoFileSystem = mod => {
|
|
18
|
-
const lib = mod?.default ?? mod;
|
|
19
|
-
if (typeof lib?.File !== 'function' || typeof lib.File.prototype?.write !== 'function') {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
const {
|
|
23
|
-
File,
|
|
24
|
-
Directory,
|
|
25
|
-
Paths
|
|
26
|
-
} = lib;
|
|
27
|
-
const documents = asDirectoryPath(Paths.document.uri);
|
|
28
|
-
return {
|
|
29
|
-
dirs: {
|
|
30
|
-
documents,
|
|
31
|
-
cache: asDirectoryPath(Paths.cache.uri),
|
|
32
|
-
// expo exposes no public Downloads directory, so a "downloads" request
|
|
33
|
-
// lands in the app's documents directory instead of the shared folder.
|
|
34
|
-
downloads: documents
|
|
35
|
-
},
|
|
36
|
-
write: (path, bytes) => {
|
|
37
|
-
const file = new File(path);
|
|
38
|
-
file.create({
|
|
39
|
-
intermediates: true,
|
|
40
|
-
overwrite: true
|
|
41
|
-
});
|
|
42
|
-
file.write(bytes);
|
|
43
|
-
return Promise.resolve();
|
|
44
|
-
},
|
|
45
|
-
exists: path => Promise.resolve(new File(path).exists),
|
|
46
|
-
stat: path => Promise.resolve({
|
|
47
|
-
size: new File(path).size
|
|
48
|
-
}),
|
|
49
|
-
unlink: path => {
|
|
50
|
-
new File(path).delete();
|
|
51
|
-
return Promise.resolve();
|
|
52
|
-
},
|
|
53
|
-
// The download module calls this for a directory that usually already
|
|
54
|
-
// exists, so it must be idempotent rather than throw on the second run.
|
|
55
|
-
mkdir: path => {
|
|
56
|
-
new Directory(path).create({
|
|
57
|
-
intermediates: true,
|
|
58
|
-
idempotent: true
|
|
59
|
-
});
|
|
60
|
-
return Promise.resolve();
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
};
|
|
64
|
-
//# sourceMappingURL=expoFs.js.map
|
|
1
|
+
const asDirectoryPath=e=>e.replace(/\/+$/,"");export const adaptExpoFileSystem=e=>{const t=e?.default??e;if("function"!=typeof t?.File||"function"!=typeof t.File.prototype?.write)return null;const{File:r,Directory:i,Paths:o}=t,s=asDirectoryPath(o.document.uri);return{dirs:{documents:s,cache:asDirectoryPath(o.cache.uri),downloads:s},write:(e,t)=>{const i=new r(e);return i.create({intermediates:!0,overwrite:!0}),i.write(t),Promise.resolve()},exists:e=>Promise.resolve(new r(e).exists),stat:e=>Promise.resolve({size:new r(e).size}),unlink:e=>(new r(e).delete(),Promise.resolve()),mkdir:e=>(new i(e).create({intermediates:!0,idempotent:!0}),Promise.resolve())}};
|
|
@@ -1,8 +1 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
/** Coerce a driver-supplied size, which some report as a string. */
|
|
4
|
-
export const asNumber = value => {
|
|
5
|
-
const n = typeof value === 'number' ? value : Number(value);
|
|
6
|
-
return Number.isFinite(n) ? n : 0;
|
|
7
|
-
};
|
|
8
|
-
//# sourceMappingURL=shared.js.map
|
|
1
|
+
export const asNumber=e=>{const r="number"==typeof e?e:Number(e);return Number.isFinite(r)?r:0};
|
|
@@ -1,242 +1 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
import * as ExpoFileSystem from 'expo-file-system';
|
|
4
|
-
import { Platform } from 'react-native';
|
|
5
|
-
import { Logger } from "../logger/index.js";
|
|
6
|
-
import { Permissions } from "../permissions/index.js";
|
|
7
|
-
import { adaptExpoFileSystem } from "./adapters/expoFs.js";
|
|
8
|
-
import { asNumber } from "./adapters/shared.js";
|
|
9
|
-
const PREFIX = '[@webority/mobile-core]';
|
|
10
|
-
|
|
11
|
-
/** Android stopped requiring a storage permission for a Downloads write at API 29. */
|
|
12
|
-
const SCOPED_STORAGE_API_LEVEL = 29;
|
|
13
|
-
|
|
14
|
-
/** A file can take far longer than the 30 s the fleet's JSON clients allow. */
|
|
15
|
-
const DOWNLOAD_TIMEOUT_MS = 300000;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The slice of a filesystem module this module actually uses. expo-file-system
|
|
19
|
-
* is adapted onto it, and a test or the showcase can inject its own with
|
|
20
|
-
* `setFileSystemImplementation`.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The slice of an HTTP client a download needs. The app's axios instance
|
|
25
|
-
* (mobile-core's `userClient` or `publicClient`, or its own) satisfies it as is,
|
|
26
|
-
* so whatever that client adds to a request (bearer token, tenant header, refresh
|
|
27
|
-
* and retry on 401) applies to the download too. Declared structurally, as
|
|
28
|
-
* mobile-support's client is, so no axios type leaks into the declarations.
|
|
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 emptyFile = url => new DownloadError('empty-file', `The download of ${url} produced an empty file, so nothing was saved. ` + 'Check that the URL is correct and that the request carried the credentials the ' + 'server expects.');
|
|
122
|
-
const requestFailed = (error, options, fileName) => {
|
|
123
|
-
if (options.signal?.aborted) {
|
|
124
|
-
return new DownloadError('aborted', `The download of ${fileName} was aborted.`);
|
|
125
|
-
}
|
|
126
|
-
const e = error ?? {};
|
|
127
|
-
// By name, not instanceof: a second copy of mobile-core would fail instanceof.
|
|
128
|
-
if (e.name === 'RequestOfflineError') {
|
|
129
|
-
return new DownloadError('network', `The device is offline, so ${options.url} was not downloaded.`);
|
|
130
|
-
}
|
|
131
|
-
const status = e.response?.status;
|
|
132
|
-
if (typeof status === 'number') {
|
|
133
|
-
return new DownloadError('http-error', `The server answered ${status} for ${options.url}, so no file was saved.`, status);
|
|
134
|
-
}
|
|
135
|
-
return new DownloadError('network', `The download of ${options.url} failed: ${describe(error)}`);
|
|
136
|
-
};
|
|
137
|
-
const discardPartial = async (fs, path) => {
|
|
138
|
-
try {
|
|
139
|
-
if (await fs.exists(path)) {
|
|
140
|
-
await fs.unlink(path);
|
|
141
|
-
}
|
|
142
|
-
} catch (error) {
|
|
143
|
-
Logger.warn(`${PREFIX} Failed to remove the partial download at ${path}`, error);
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Download a file to the device through the app's own HTTP client, so it is
|
|
149
|
-
* authenticated exactly as the app's other calls are. The whole file is held
|
|
150
|
-
* in memory before it is written, which suits documents and reports rather
|
|
151
|
-
* than large media.
|
|
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
|
-
* reported as `empty-file` and nothing is kept.
|
|
156
|
-
*/
|
|
157
|
-
export 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: asNumber(stat?.size),
|
|
184
|
-
mimeType: options.mimeType,
|
|
185
|
-
fromCache: true
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
const {
|
|
189
|
-
onProgress
|
|
190
|
-
} = options;
|
|
191
|
-
let data;
|
|
192
|
-
try {
|
|
193
|
-
({
|
|
194
|
-
data
|
|
195
|
-
} = await options.client.get(options.url, {
|
|
196
|
-
headers: {
|
|
197
|
-
Accept: '*/*',
|
|
198
|
-
...options.headers
|
|
199
|
-
},
|
|
200
|
-
responseType: 'arraybuffer',
|
|
201
|
-
timeout: DOWNLOAD_TIMEOUT_MS,
|
|
202
|
-
signal: options.signal,
|
|
203
|
-
onDownloadProgress: onProgress ? ({
|
|
204
|
-
loaded,
|
|
205
|
-
total = 0
|
|
206
|
-
}) => onProgress(loaded, total, total > 0 ? loaded / total : 0) : undefined
|
|
207
|
-
}));
|
|
208
|
-
} catch (error) {
|
|
209
|
-
throw requestFailed(error, options, fileName);
|
|
210
|
-
}
|
|
211
|
-
if (options.signal?.aborted) {
|
|
212
|
-
throw new DownloadError('aborted', `The download of ${fileName} was aborted.`);
|
|
213
|
-
}
|
|
214
|
-
const received = new Uint8Array(data ?? new ArrayBuffer(0));
|
|
215
|
-
if (received.byteLength === 0) {
|
|
216
|
-
throw emptyFile(options.url);
|
|
217
|
-
}
|
|
218
|
-
try {
|
|
219
|
-
await fs.write(path, received);
|
|
220
|
-
} catch (error) {
|
|
221
|
-
await discardPartial(fs, path);
|
|
222
|
-
throw new DownloadError('write-failed', `Could not write ${path}: ${describe(error)}`);
|
|
223
|
-
}
|
|
224
|
-
let bytes = 0;
|
|
225
|
-
try {
|
|
226
|
-
bytes = (await fs.exists(path)) ? asNumber((await fs.stat(path)).size) : 0;
|
|
227
|
-
} catch (error) {
|
|
228
|
-
await discardPartial(fs, path);
|
|
229
|
-
throw new DownloadError('write-failed', `The download of ${fileName} finished but ${path} could not be read back: ${describe(error)}`);
|
|
230
|
-
}
|
|
231
|
-
if (bytes <= 0) {
|
|
232
|
-
await discardPartial(fs, path);
|
|
233
|
-
throw emptyFile(options.url);
|
|
234
|
-
}
|
|
235
|
-
return {
|
|
236
|
-
path,
|
|
237
|
-
bytes,
|
|
238
|
-
mimeType: options.mimeType,
|
|
239
|
-
fromCache: false
|
|
240
|
-
};
|
|
241
|
-
};
|
|
242
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import*as e from"expo-file-system";import{Platform as o}from"react-native";import{Logger as t}from"../logger/index.js";import{Permissions as r}from"../permissions/index.js";import{adaptExpoFileSystem as n}from"./adapters/expoFs.js";import{asNumber as a}from"./adapters/shared.js";const i="[@webority/mobile-core]";export class DownloadError extends Error{constructor(e,o,t){super(`${i} ${o}`),this.name="DownloadError",this.code=e,this.status=t,Object.setPrototypeOf(this,DownloadError.prototype)}}const describe=e=>e instanceof Error?e.message:String(e);let s,d=null;export const setFileSystemImplementation=e=>{d=e};const requireBackend=()=>{const o=d||(void 0===s&&(s=n(e)),s);if(!o)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.");return o},lastSegment=e=>{const o=e.split(/[/\\]/);return(t=o[o.length-1]??"",Array.from(t).filter(e=>(e.codePointAt(0)??0)>31).join("")).trim();var t},ensureStoragePermission=async e=>{if("android"!==o.OS||"downloads"!==e)return;if(("number"==typeof o.Version?o.Version:Number.parseInt(String(o.Version),10))>=29)return;const t=await r.ensure("storage");if("granted"!==t&&"limited"!==t)throw new DownloadError("permission-denied",`Storage permission is "${t}", 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.`)},emptyFile=e=>new DownloadError("empty-file",`The download of ${e} produced an empty file, so nothing was saved. Check that the URL is correct and that the request carried the credentials the server expects.`),discardPartial=async(e,o)=>{try{await e.exists(o)&&await e.unlink(o)}catch(e){t.warn(`${i} Failed to remove the partial download at ${o}`,e)}};export const downloadFile=async e=>{const o=requireBackend(),t=e.destination??"documents",r=e.fileName?(e=>{const o=lastSegment(e);if(""===o||"."===o||".."===o)throw new DownloadError("write-failed",`"${e}" does not contain a usable file name. Pass an explicit fileName.`);return o})(e.fileName):(e=>{const o=(e.split("#")[0]?.split("?")[0]??"").split("/").pop()??"";let t=o;try{t=decodeURIComponent(o)}catch{}const r=lastSegment(t);return""===r||"."===r||".."===r?"download":r})(e.url);await ensureStoragePermission(t);const n=o.dirs[t],i=`${n}/${r}`;if(e.signal?.aborted)throw new DownloadError("aborted",`The download of ${r} was aborted before it started.`);let s=!1;try{await o.mkdir(n);const t=await o.exists(i);t&&!1===e.overwrite?s=!0:t&&await o.unlink(i)}catch(e){throw new DownloadError("write-failed",`Could not prepare ${i} for writing: ${describe(e)}`)}if(s){const t=await o.stat(i);return{path:i,bytes:a(t?.size),mimeType:e.mimeType,fromCache:!0}}const{onProgress:d}=e;let l;try{({data:l}=await e.client.get(e.url,{headers:{Accept:"*/*",...e.headers},responseType:"arraybuffer",timeout:3e5,signal:e.signal,onDownloadProgress:d?({loaded:e,total:o=0})=>d(e,o,o>0?e/o:0):void 0}))}catch(o){throw((e,o,t)=>{if(o.signal?.aborted)return new DownloadError("aborted",`The download of ${t} was aborted.`);const r=e??{};if("RequestOfflineError"===r.name)return new DownloadError("network",`The device is offline, so ${o.url} was not downloaded.`);const n=r.response?.status;return"number"==typeof n?new DownloadError("http-error",`The server answered ${n} for ${o.url}, so no file was saved.`,n):new DownloadError("network",`The download of ${o.url} failed: ${describe(e)}`)})(o,e,r)}if(e.signal?.aborted)throw new DownloadError("aborted",`The download of ${r} was aborted.`);const w=new Uint8Array(l??new ArrayBuffer(0));if(0===w.byteLength)throw emptyFile(e.url);try{await o.write(i,w)}catch(e){throw await discardPartial(o,i),new DownloadError("write-failed",`Could not write ${i}: ${describe(e)}`)}let c=0;try{c=await o.exists(i)?a((await o.stat(i)).size):0}catch(e){throw await discardPartial(o,i),new DownloadError("write-failed",`The download of ${r} finished but ${i} could not be read back: ${describe(e)}`)}if(c<=0)throw await discardPartial(o,i),emptyFile(e.url);return{path:i,bytes:c,mimeType:e.mimeType,fromCache:!1}};
|