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