@webority-technologies/mobile-ui 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/lib/commonjs/components/AuthedImage/AuthedImage.js +225 -0
- package/lib/commonjs/components/AuthedImage/authedImageCache.js +253 -0
- package/lib/commonjs/components/AuthedImage/index.js +45 -0
- package/lib/commonjs/components/BottomSheet/BottomSheet.js +10 -4
- package/lib/commonjs/components/Drawer/Drawer.js +6 -3
- package/lib/commonjs/components/ErrorBoundary/ErrorBoundary.js +136 -0
- package/lib/commonjs/components/ErrorBoundary/index.js +27 -0
- package/lib/commonjs/components/ErrorBoundary/useErrorHandler.js +24 -0
- package/lib/commonjs/components/ErrorBoundary/withErrorBoundary.js +28 -0
- package/lib/commonjs/components/FilePicker/FilePicker.js +1 -1
- package/lib/commonjs/components/FloatingActionButton/FloatingActionButton.js +14 -1
- package/lib/commonjs/components/ImageGallery/ImageGallery.js +13 -0
- package/lib/commonjs/components/InlineSelect/InlineSelect.js +1 -1
- package/lib/commonjs/components/Input/Input.js +6 -0
- package/lib/commonjs/components/KeyboardAwareScrollView/KeyboardAwareScrollView.js +62 -13
- package/lib/commonjs/components/LoaderOverlay/LoaderOverlay.js +203 -0
- package/lib/commonjs/components/LoaderOverlay/index.js +25 -0
- package/lib/commonjs/components/Markdown/Markdown.js +274 -0
- package/lib/commonjs/components/Markdown/index.js +13 -0
- package/lib/commonjs/components/Markdown/parse.js +229 -0
- package/lib/commonjs/components/OTPInput/OTPInput.js +5 -0
- package/lib/commonjs/components/OptionSheet/OptionSheet.js +3 -3
- package/lib/commonjs/components/PhoneNumberInput/PhoneNumberInput.js +1 -1
- package/lib/commonjs/components/Radio/Radio.js +9 -0
- package/lib/commonjs/components/Rating/Rating.js +5 -0
- package/lib/commonjs/components/RichTextEditor/RichTextEditor.js +311 -0
- package/lib/commonjs/components/RichTextEditor/index.js +20 -0
- package/lib/commonjs/components/RichTextEditor/markdownActions.js +213 -0
- package/lib/commonjs/components/SearchBar/SearchBar.js +7 -0
- package/lib/commonjs/components/Select/Select.js +1 -1
- package/lib/commonjs/components/Skeleton/Skeleton.js +17 -36
- package/lib/commonjs/components/Slider/Slider.js +21 -5
- package/lib/commonjs/components/Swipeable/Swipeable.js +5 -2
- package/lib/commonjs/components/TimePicker/TimePicker.js +11 -0
- package/lib/commonjs/components/index.js +230 -141
- package/lib/commonjs/form/FormContext.js +13 -1
- package/lib/commonjs/form/useField.js +13 -1
- package/lib/commonjs/theme/Gradient.js +44 -41
- package/lib/commonjs/theme/index.js +24 -2
- package/lib/commonjs/utils/hapticUtils.js +38 -18
- package/lib/module/components/AuthedImage/AuthedImage.js +221 -0
- package/lib/module/components/AuthedImage/authedImageCache.js +239 -0
- package/lib/module/components/AuthedImage/index.js +5 -0
- package/lib/module/components/BottomSheet/BottomSheet.js +10 -4
- package/lib/module/components/Drawer/Drawer.js +6 -3
- package/lib/module/components/ErrorBoundary/ErrorBoundary.js +130 -0
- package/lib/module/components/ErrorBoundary/index.js +6 -0
- package/lib/module/components/ErrorBoundary/useErrorHandler.js +20 -0
- package/lib/module/components/ErrorBoundary/withErrorBoundary.js +23 -0
- package/lib/module/components/FilePicker/FilePicker.js +1 -1
- package/lib/module/components/FloatingActionButton/FloatingActionButton.js +14 -1
- package/lib/module/components/ImageGallery/ImageGallery.js +13 -0
- package/lib/module/components/InlineSelect/InlineSelect.js +1 -1
- package/lib/module/components/Input/Input.js +6 -0
- package/lib/module/components/KeyboardAwareScrollView/KeyboardAwareScrollView.js +63 -14
- package/lib/module/components/LoaderOverlay/LoaderOverlay.js +196 -0
- package/lib/module/components/LoaderOverlay/index.js +4 -0
- package/lib/module/components/Markdown/Markdown.js +268 -0
- package/lib/module/components/Markdown/index.js +4 -0
- package/lib/module/components/Markdown/parse.js +223 -0
- package/lib/module/components/OTPInput/OTPInput.js +5 -0
- package/lib/module/components/OptionSheet/OptionSheet.js +3 -3
- package/lib/module/components/PhoneNumberInput/PhoneNumberInput.js +1 -1
- package/lib/module/components/Radio/Radio.js +9 -0
- package/lib/module/components/Rating/Rating.js +5 -0
- package/lib/module/components/RichTextEditor/RichTextEditor.js +306 -0
- package/lib/module/components/RichTextEditor/index.js +5 -0
- package/lib/module/components/RichTextEditor/markdownActions.js +207 -0
- package/lib/module/components/SearchBar/SearchBar.js +7 -0
- package/lib/module/components/Select/Select.js +1 -1
- package/lib/module/components/Skeleton/Skeleton.js +17 -36
- package/lib/module/components/Slider/Slider.js +21 -5
- package/lib/module/components/Swipeable/Swipeable.js +5 -2
- package/lib/module/components/TimePicker/TimePicker.js +11 -0
- package/lib/module/components/index.js +5 -0
- package/lib/module/form/FormContext.js +13 -1
- package/lib/module/form/useField.js +13 -1
- package/lib/module/theme/Gradient.js +41 -38
- package/lib/module/theme/index.js +17 -1
- package/lib/module/utils/hapticUtils.js +37 -18
- package/lib/typescript/commonjs/components/AuthedImage/AuthedImage.d.ts +22 -0
- package/lib/typescript/commonjs/components/AuthedImage/authedImageCache.d.ts +48 -0
- package/lib/typescript/commonjs/components/AuthedImage/index.d.ts +5 -0
- package/lib/typescript/commonjs/components/Button/Button.d.ts +1 -3
- package/lib/typescript/commonjs/components/Card/Card.d.ts +1 -2
- package/lib/typescript/commonjs/components/ErrorBoundary/ErrorBoundary.d.ts +38 -0
- package/lib/typescript/commonjs/components/ErrorBoundary/index.d.ts +5 -0
- package/lib/typescript/commonjs/components/ErrorBoundary/useErrorHandler.d.ts +9 -0
- package/lib/typescript/commonjs/components/ErrorBoundary/withErrorBoundary.d.ts +9 -0
- package/lib/typescript/commonjs/components/KeyboardAwareScrollView/KeyboardAwareScrollView.d.ts +15 -0
- package/lib/typescript/commonjs/components/LoaderOverlay/LoaderOverlay.d.ts +25 -0
- package/lib/typescript/commonjs/components/LoaderOverlay/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/Markdown/Markdown.d.ts +29 -0
- package/lib/typescript/commonjs/components/Markdown/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/Markdown/parse.d.ts +58 -0
- package/lib/typescript/commonjs/components/OptionSheet/OptionSheet.d.ts +8 -1
- package/lib/typescript/commonjs/components/RichTextEditor/RichTextEditor.d.ts +48 -0
- package/lib/typescript/commonjs/components/RichTextEditor/index.d.ts +5 -0
- package/lib/typescript/commonjs/components/RichTextEditor/markdownActions.d.ts +35 -0
- package/lib/typescript/commonjs/components/Skeleton/Skeleton.d.ts +1 -1
- package/lib/typescript/commonjs/components/index.d.ts +10 -0
- package/lib/typescript/commonjs/form/FormContext.d.ts +13 -3
- package/lib/typescript/commonjs/theme/Gradient.d.ts +18 -3
- package/lib/typescript/commonjs/theme/index.d.ts +16 -1
- package/lib/typescript/commonjs/theme/types.d.ts +2 -2
- package/lib/typescript/commonjs/utils/hapticUtils.d.ts +4 -0
- package/lib/typescript/module/components/AuthedImage/AuthedImage.d.ts +22 -0
- package/lib/typescript/module/components/AuthedImage/authedImageCache.d.ts +48 -0
- package/lib/typescript/module/components/AuthedImage/index.d.ts +5 -0
- package/lib/typescript/module/components/Button/Button.d.ts +1 -3
- package/lib/typescript/module/components/Card/Card.d.ts +1 -2
- package/lib/typescript/module/components/ErrorBoundary/ErrorBoundary.d.ts +38 -0
- package/lib/typescript/module/components/ErrorBoundary/index.d.ts +5 -0
- package/lib/typescript/module/components/ErrorBoundary/useErrorHandler.d.ts +9 -0
- package/lib/typescript/module/components/ErrorBoundary/withErrorBoundary.d.ts +9 -0
- package/lib/typescript/module/components/KeyboardAwareScrollView/KeyboardAwareScrollView.d.ts +15 -0
- package/lib/typescript/module/components/LoaderOverlay/LoaderOverlay.d.ts +25 -0
- package/lib/typescript/module/components/LoaderOverlay/index.d.ts +3 -0
- package/lib/typescript/module/components/Markdown/Markdown.d.ts +29 -0
- package/lib/typescript/module/components/Markdown/index.d.ts +3 -0
- package/lib/typescript/module/components/Markdown/parse.d.ts +58 -0
- package/lib/typescript/module/components/OptionSheet/OptionSheet.d.ts +8 -1
- package/lib/typescript/module/components/RichTextEditor/RichTextEditor.d.ts +48 -0
- package/lib/typescript/module/components/RichTextEditor/index.d.ts +5 -0
- package/lib/typescript/module/components/RichTextEditor/markdownActions.d.ts +35 -0
- package/lib/typescript/module/components/Skeleton/Skeleton.d.ts +1 -1
- package/lib/typescript/module/components/index.d.ts +10 -0
- package/lib/typescript/module/form/FormContext.d.ts +13 -3
- package/lib/typescript/module/theme/Gradient.d.ts +18 -3
- package/lib/typescript/module/theme/index.d.ts +16 -1
- package/lib/typescript/module/theme/types.d.ts +2 -2
- package/lib/typescript/module/utils/hapticUtils.d.ts +4 -0
- package/package.json +20 -13
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { ACCESS_TOKEN_KEY, getToken, isStorageAvailable, Logger, Storage } from '@webority-technologies/mobile-core';
|
|
4
|
+
const KEY_PREFIX = '@webority/authed-image/v1/';
|
|
5
|
+
const DATA_PREFIX = `${KEY_PREFIX}d/`;
|
|
6
|
+
const META_PREFIX = `${KEY_PREFIX}m/`;
|
|
7
|
+
const DEFAULT_BYTE_CEILING = 50 * 1024 * 1024;
|
|
8
|
+
export const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
+
let byteCeiling = DEFAULT_BYTE_CEILING;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ceiling for the total stored size of cached images. Exceeding it evicts the
|
|
13
|
+
* least recently used entries. An image cache with no ceiling grows until the
|
|
14
|
+
* OS reclaims the app's storage, which is the defect this replaces.
|
|
15
|
+
*/
|
|
16
|
+
export const setAuthedImageCacheLimit = bytes => {
|
|
17
|
+
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
18
|
+
throw new Error('[@webority-technologies/mobile-ui] setAuthedImageCacheLimit() needs a positive number of ' + `bytes, received ${String(bytes)}. Pass e.g. 50 * 1024 * 1024 for 50 MB.`);
|
|
19
|
+
}
|
|
20
|
+
byteCeiling = bytes;
|
|
21
|
+
};
|
|
22
|
+
export const getAuthedImageCacheLimit = () => byteCeiling;
|
|
23
|
+
const hashString = input => {
|
|
24
|
+
let h1 = 0x811c9dc5;
|
|
25
|
+
let h2 = 0x1000193;
|
|
26
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
27
|
+
const code = input.charCodeAt(i);
|
|
28
|
+
h1 = Math.imul(h1 ^ code, 16777619) >>> 0;
|
|
29
|
+
h2 = (Math.imul(h2 + code, 2654435761) ^ h2 >>> 7) >>> 0;
|
|
30
|
+
}
|
|
31
|
+
return `${h1.toString(16)}${h2.toString(16)}${input.length.toString(16)}`;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Hex only by construction, so it is always a safe storage key segment.
|
|
36
|
+
*/
|
|
37
|
+
export const deriveCacheKey = (uri, override) => hashString(override ?? uri);
|
|
38
|
+
const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
39
|
+
const encodeBase64 = buffer => {
|
|
40
|
+
const bytes = new Uint8Array(buffer);
|
|
41
|
+
let out = '';
|
|
42
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
43
|
+
const b0 = bytes[i];
|
|
44
|
+
const b1 = i + 1 < bytes.length ? bytes[i + 1] : undefined;
|
|
45
|
+
const b2 = i + 2 < bytes.length ? bytes[i + 2] : undefined;
|
|
46
|
+
out += B64_ALPHABET[b0 >> 2];
|
|
47
|
+
out += B64_ALPHABET[(b0 & 0x03) << 4 | (b1 ?? 0) >> 4];
|
|
48
|
+
out += b1 === undefined ? '=' : B64_ALPHABET[((b1 & 0x0f) << 2 | (b2 ?? 0) >> 6) & 0x3f];
|
|
49
|
+
out += b2 === undefined ? '=' : B64_ALPHABET[b2 & 0x3f];
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The cache is an OPTIMISATION, so a failing store must never stop an image
|
|
56
|
+
* rendering. That used to mean guarding on whether a backend existed at all,
|
|
57
|
+
* warning once and skipping the cache; under Expo-first the backend ships with
|
|
58
|
+
* the library, so "no backend" is no longer a state an app can be in and that
|
|
59
|
+
* branch was unreachable.
|
|
60
|
+
*
|
|
61
|
+
* What remains is a store that exists and FAILS, which the per-operation error
|
|
62
|
+
* handling below already covers: each read returns null and each write returns
|
|
63
|
+
* false, the image still fetches, and nothing is logged per image.
|
|
64
|
+
*/
|
|
65
|
+
const cacheUsable = () => isStorageAvailable();
|
|
66
|
+
export const readEntry = async cacheKey => {
|
|
67
|
+
if (!cacheUsable()) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const values = await Storage.multiGet([`${DATA_PREFIX}${cacheKey}`, `${META_PREFIX}${cacheKey}`]);
|
|
71
|
+
const dataUri = values[`${DATA_PREFIX}${cacheKey}`];
|
|
72
|
+
const rawMeta = values[`${META_PREFIX}${cacheKey}`];
|
|
73
|
+
if (!dataUri || !rawMeta) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return {
|
|
78
|
+
dataUri,
|
|
79
|
+
meta: JSON.parse(rawMeta)
|
|
80
|
+
};
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
export const isFresh = (meta, ttlMs, now = Date.now()) => now - meta.fetchedAt < ttlMs;
|
|
86
|
+
export const touchEntry = async (cacheKey, meta) => {
|
|
87
|
+
if (!cacheUsable()) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await Storage.setItem(`${META_PREFIX}${cacheKey}`, JSON.stringify({
|
|
91
|
+
...meta,
|
|
92
|
+
lastAccessedAt: Date.now()
|
|
93
|
+
}));
|
|
94
|
+
};
|
|
95
|
+
const listMeta = async () => {
|
|
96
|
+
if (!cacheUsable()) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const keys = await Storage.getAllKeys();
|
|
100
|
+
const metaKeys = keys.filter(key => key.startsWith(META_PREFIX));
|
|
101
|
+
if (metaKeys.length === 0) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const values = await Storage.multiGet(metaKeys);
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const key of metaKeys) {
|
|
107
|
+
const raw = values[key];
|
|
108
|
+
if (!raw) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
out.push({
|
|
113
|
+
cacheKey: key.slice(META_PREFIX.length),
|
|
114
|
+
meta: JSON.parse(raw)
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
// A meta entry we cannot parse is unusable; dropping it from the ledger
|
|
118
|
+
// lets the next eviction pass remove its orphaned data entry.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
};
|
|
123
|
+
const evictIfNeeded = async () => {
|
|
124
|
+
const entries = await listMeta();
|
|
125
|
+
let total = entries.reduce((sum, entry) => sum + entry.meta.bytes, 0);
|
|
126
|
+
if (total <= byteCeiling) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const ordered = [...entries].sort((a, b) => a.meta.lastAccessedAt - b.meta.lastAccessedAt);
|
|
130
|
+
const doomed = [];
|
|
131
|
+
for (const entry of ordered) {
|
|
132
|
+
if (total <= byteCeiling) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
doomed.push(`${DATA_PREFIX}${entry.cacheKey}`, `${META_PREFIX}${entry.cacheKey}`);
|
|
136
|
+
total -= entry.meta.bytes;
|
|
137
|
+
}
|
|
138
|
+
if (doomed.length > 0) {
|
|
139
|
+
await Storage.multiRemove(doomed);
|
|
140
|
+
Logger.info(`[AuthedImage] evicted ${doomed.length / 2} cached image(s) over the byte ceiling`);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
export const clearAuthedImageCache = async () => {
|
|
144
|
+
if (!cacheUsable()) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const keys = await Storage.getAllKeys();
|
|
148
|
+
const ours = keys.filter(key => key.startsWith(KEY_PREFIX));
|
|
149
|
+
if (ours.length > 0) {
|
|
150
|
+
await Storage.multiRemove(ours);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
export const authedImageCacheStats = async () => {
|
|
154
|
+
const entries = await listMeta();
|
|
155
|
+
return {
|
|
156
|
+
entries: entries.length,
|
|
157
|
+
bytes: entries.reduce((sum, entry) => sum + entry.meta.bytes, 0)
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
const fetchAndStore = async (cacheKey, options, signal) => {
|
|
161
|
+
const headers = {
|
|
162
|
+
...options.headers
|
|
163
|
+
};
|
|
164
|
+
if (options.authenticated) {
|
|
165
|
+
const stored = await getToken(ACCESS_TOKEN_KEY);
|
|
166
|
+
if (stored?.token) {
|
|
167
|
+
headers.Authorization = `Bearer ${stored.token}`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const response = await fetch(options.uri, {
|
|
171
|
+
headers,
|
|
172
|
+
signal
|
|
173
|
+
});
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
throw new Error(`[@webority-technologies/mobile-ui] AuthedImage could not load "${options.uri}": the server ` + `answered ${response.status}. Check the URL and whether the request needs a bearer token ` + '(pass authenticated={false} for public media).');
|
|
176
|
+
}
|
|
177
|
+
const buffer = await response.arrayBuffer();
|
|
178
|
+
const rawType = response.headers?.get?.('content-type');
|
|
179
|
+
const contentType = (rawType ? rawType.split(';')[0].trim() : '') || 'image/jpeg';
|
|
180
|
+
const dataUri = `data:${contentType};base64,${encodeBase64(buffer)}`;
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
// `bytes` measures the stored data URI, not the decoded image: the ceiling
|
|
183
|
+
// exists to bound what sits in storage, and base64 costs about a third more.
|
|
184
|
+
const meta = {
|
|
185
|
+
fetchedAt: now,
|
|
186
|
+
bytes: dataUri.length,
|
|
187
|
+
contentType,
|
|
188
|
+
lastAccessedAt: now
|
|
189
|
+
};
|
|
190
|
+
if (cacheUsable()) {
|
|
191
|
+
await Storage.multiSet({
|
|
192
|
+
[`${DATA_PREFIX}${cacheKey}`]: dataUri,
|
|
193
|
+
[`${META_PREFIX}${cacheKey}`]: JSON.stringify(meta)
|
|
194
|
+
});
|
|
195
|
+
await evictIfNeeded();
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
dataUri,
|
|
199
|
+
meta
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
const inFlight = new Map();
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Two mounts of the same image share one request. The controller is aborted
|
|
206
|
+
* only when the last waiter releases, so one unmount cannot cancel the fetch
|
|
207
|
+
* another mount is still waiting on.
|
|
208
|
+
*/
|
|
209
|
+
export const acquireFetch = (cacheKey, options) => {
|
|
210
|
+
const existing = inFlight.get(cacheKey);
|
|
211
|
+
if (existing) {
|
|
212
|
+
existing.waiters += 1;
|
|
213
|
+
return existing;
|
|
214
|
+
}
|
|
215
|
+
const controller = new AbortController();
|
|
216
|
+
const entry = {
|
|
217
|
+
controller,
|
|
218
|
+
waiters: 1,
|
|
219
|
+
promise: Promise.resolve()
|
|
220
|
+
};
|
|
221
|
+
entry.promise = fetchAndStore(cacheKey, options, controller.signal).finally(() => {
|
|
222
|
+
if (inFlight.get(cacheKey) === entry) {
|
|
223
|
+
inFlight.delete(cacheKey);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
inFlight.set(cacheKey, entry);
|
|
227
|
+
return entry;
|
|
228
|
+
};
|
|
229
|
+
export const releaseFetch = (cacheKey, entry) => {
|
|
230
|
+
entry.waiters -= 1;
|
|
231
|
+
if (entry.waiters > 0) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (inFlight.get(cacheKey) === entry) {
|
|
235
|
+
inFlight.delete(cacheKey);
|
|
236
|
+
}
|
|
237
|
+
entry.controller.abort();
|
|
238
|
+
};
|
|
239
|
+
//# sourceMappingURL=authedImageCache.js.map
|
|
@@ -120,6 +120,12 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
120
120
|
|
|
121
121
|
// Convert a snap-point index → translateY position. -1 = closed.
|
|
122
122
|
const yForIndex = useCallback(idx => {
|
|
123
|
+
// NOT dead code, despite every ordinary call site clamping first.
|
|
124
|
+
// `expand()` has no empty-snapPoints guard (unlike `collapse`), so with
|
|
125
|
+
// snapPoints={[]} it arrives here out of range — and without this,
|
|
126
|
+
// `screenHeight - resolvedSnapPoints[idx]` is `screenHeight - undefined`,
|
|
127
|
+
// i.e. NaN into the sheet position, which breaks layout silently rather
|
|
128
|
+
// than degrading. Covered by the "degenerate snapPoints" tests.
|
|
123
129
|
if (idx < 0 || idx >= resolvedSnapPoints.length) {
|
|
124
130
|
return closedY;
|
|
125
131
|
}
|
|
@@ -256,7 +262,7 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
256
262
|
const closed = closedY;
|
|
257
263
|
const top = minTopY;
|
|
258
264
|
const canPanDownToClose = enablePanDownToClose;
|
|
259
|
-
return Gesture.Pan().activeOffsetY([-DRAG_ACTIVATION_OFFSET, DRAG_ACTIVATION_OFFSET]).failOffsetX([-15, 15]).onStart(() => {
|
|
265
|
+
return Gesture.Pan().withTestId(testID ? `${testID}-pan-gesture` : 'bottom-sheet-pan-gesture').activeOffsetY([-DRAG_ACTIVATION_OFFSET, DRAG_ACTIVATION_OFFSET]).failOffsetX([-15, 15]).onStart(() => {
|
|
260
266
|
'worklet';
|
|
261
267
|
|
|
262
268
|
dragStartY.value = translateY.value;
|
|
@@ -306,7 +312,7 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
306
312
|
}
|
|
307
313
|
runOnJS(handleSnapEnd)(nearestIdx, targets[nearestIdx]);
|
|
308
314
|
});
|
|
309
|
-
}, [closedY, dragStartY, enablePanDownToClose, handleSnapEnd, minTopY, snapTargets, translateY]);
|
|
315
|
+
}, [closedY, dragStartY, enablePanDownToClose, handleSnapEnd, minTopY, snapTargets, testID, translateY]);
|
|
310
316
|
|
|
311
317
|
// ───────── Keyboard tracking ─────────
|
|
312
318
|
// keyboardOffset is added to translateY in sheetStyle so we don't disturb
|
|
@@ -378,14 +384,14 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
378
384
|
// gesture-handler/reanimated sheet doesn't fire reliably on RN 0.85 / Fabric
|
|
379
385
|
// (the gesture system swallows the touch sequence). gorhom/bottom-sheet uses
|
|
380
386
|
// the same Gesture.Tap() pattern for the same reason.
|
|
381
|
-
const backdropTapGesture = useMemo(() => Gesture.Tap().maxDuration(250).onEnd((_e, success) => {
|
|
387
|
+
const backdropTapGesture = useMemo(() => Gesture.Tap().withTestId(testID ? `${testID}-backdrop-gesture` : 'bottom-sheet-backdrop-gesture').maxDuration(250).onEnd((_e, success) => {
|
|
382
388
|
'worklet';
|
|
383
389
|
|
|
384
390
|
if (!success) {
|
|
385
391
|
return;
|
|
386
392
|
}
|
|
387
393
|
runOnJS(handleBackdropPress)();
|
|
388
|
-
}), [handleBackdropPress]);
|
|
394
|
+
}), [handleBackdropPress, testID]);
|
|
389
395
|
const contextValue = useMemo(() => ({
|
|
390
396
|
snapIndex: currentIndex,
|
|
391
397
|
snapPoints: resolvedSnapPoints,
|
|
@@ -77,8 +77,12 @@ const Drawer = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
77
77
|
return sheetSize;
|
|
78
78
|
}
|
|
79
79
|
}, [side, sheetSize]);
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
|
|
81
|
+
// Uncontrolled defaultVisible starts already open — the offset must start at the
|
|
82
|
+
// open position (0) too, or the sheet is logically open while visually still
|
|
83
|
+
// pinned at the closed edge until the next explicit open()/close()/drag.
|
|
84
|
+
const offset = useSharedValue(defaultVisible ? 0 : closedOffset);
|
|
85
|
+
const dragStart = useSharedValue(defaultVisible ? 0 : closedOffset);
|
|
82
86
|
const [isOpen, setIsOpen] = useState(defaultVisible);
|
|
83
87
|
const isOpenShared = useSharedValue(defaultVisible);
|
|
84
88
|
|
|
@@ -330,7 +334,6 @@ const Drawer = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
330
334
|
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
331
335
|
accessibilityRole: "alert",
|
|
332
336
|
accessibilityLabel: accessibilityLabel ?? 'Drawer',
|
|
333
|
-
accessibilityViewIsModal: isOpen,
|
|
334
337
|
style: [sideLayout, {
|
|
335
338
|
backgroundColor: theme.colors.background.elevated,
|
|
336
339
|
...theme.shadows.xl
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { Logger } from '@webority-technologies/mobile-core';
|
|
4
|
+
import React, { useMemo } from 'react';
|
|
5
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
6
|
+
import { useTheme } from "../../theme/index.js";
|
|
7
|
+
import { EmptyState } from "../EmptyState/index.js";
|
|
8
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
|
+
const COMPONENT_STACK_LIMIT = 500;
|
|
10
|
+
const DEFAULT_TITLE = 'Something went wrong';
|
|
11
|
+
const DEFAULT_MESSAGE = 'An unexpected error occurred. Please try again.';
|
|
12
|
+
const hasResetKeysChanged = (prev, next) => {
|
|
13
|
+
if (prev === next) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (!prev || !next || prev.length !== next.length) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return prev.some((value, index) => value !== next[index]);
|
|
20
|
+
};
|
|
21
|
+
const DefaultFallback = ({
|
|
22
|
+
error,
|
|
23
|
+
reset,
|
|
24
|
+
title,
|
|
25
|
+
message,
|
|
26
|
+
testID
|
|
27
|
+
}) => {
|
|
28
|
+
const theme = useTheme();
|
|
29
|
+
const styles = useMemo(() => buildStyles(theme), [theme]);
|
|
30
|
+
const showRawError = typeof __DEV__ !== 'undefined' && __DEV__;
|
|
31
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
32
|
+
style: styles.root,
|
|
33
|
+
testID: testID,
|
|
34
|
+
children: [/*#__PURE__*/_jsx(EmptyState, {
|
|
35
|
+
title: title ?? DEFAULT_TITLE,
|
|
36
|
+
description: message ?? DEFAULT_MESSAGE,
|
|
37
|
+
action: {
|
|
38
|
+
label: 'Try again',
|
|
39
|
+
onPress: reset,
|
|
40
|
+
testID: testID ? `${testID}-retry` : undefined
|
|
41
|
+
}
|
|
42
|
+
}), showRawError ? /*#__PURE__*/_jsx(Text, {
|
|
43
|
+
style: styles.devError,
|
|
44
|
+
testID: testID ? `${testID}-dev-error` : undefined,
|
|
45
|
+
children: error.message
|
|
46
|
+
}) : null]
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
const buildStyles = theme => StyleSheet.create({
|
|
50
|
+
root: {
|
|
51
|
+
flex: 1,
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
justifyContent: 'center',
|
|
54
|
+
backgroundColor: theme.colors.background.primary,
|
|
55
|
+
padding: theme.spacing.lg
|
|
56
|
+
},
|
|
57
|
+
devError: {
|
|
58
|
+
marginTop: theme.spacing.md,
|
|
59
|
+
color: theme.colors.error,
|
|
60
|
+
fontSize: theme.typography.fontSize.sm,
|
|
61
|
+
textAlign: 'center'
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Catches render errors in its subtree and shows a themed fallback instead of
|
|
67
|
+
* a white screen. Mount this once near the navigation root per the fleet
|
|
68
|
+
* convention, and optionally again around any subtree that should recover
|
|
69
|
+
* independently (a single failed card should not blank the whole screen).
|
|
70
|
+
*/
|
|
71
|
+
export class ErrorBoundary extends React.Component {
|
|
72
|
+
state = {
|
|
73
|
+
error: null
|
|
74
|
+
};
|
|
75
|
+
static getDerivedStateFromError(error) {
|
|
76
|
+
return {
|
|
77
|
+
error
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
componentDidUpdate(prevProps) {
|
|
81
|
+
if (this.state.error && hasResetKeysChanged(prevProps.resetKeys, this.props.resetKeys)) {
|
|
82
|
+
this.reset();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
componentDidCatch(error, info) {
|
|
86
|
+
const componentStack = (info.componentStack ?? '').slice(0, COMPONENT_STACK_LIMIT);
|
|
87
|
+
Logger.recordError(error, {
|
|
88
|
+
errorType: 'RenderError',
|
|
89
|
+
componentStack
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
this.props.onError?.(error, {
|
|
93
|
+
componentStack
|
|
94
|
+
});
|
|
95
|
+
} catch (onErrorError) {
|
|
96
|
+
Logger.error('[@webority-technologies/mobile-ui] ErrorBoundary onError threw; the boundary itself is unaffected.', onErrorError);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
reset = () => {
|
|
100
|
+
this.setState({
|
|
101
|
+
error: null
|
|
102
|
+
});
|
|
103
|
+
this.props.onReset?.();
|
|
104
|
+
};
|
|
105
|
+
render() {
|
|
106
|
+
const {
|
|
107
|
+
error
|
|
108
|
+
} = this.state;
|
|
109
|
+
if (!error) {
|
|
110
|
+
return this.props.children;
|
|
111
|
+
}
|
|
112
|
+
const {
|
|
113
|
+
fallback,
|
|
114
|
+
title,
|
|
115
|
+
message,
|
|
116
|
+
testID
|
|
117
|
+
} = this.props;
|
|
118
|
+
if (fallback !== undefined) {
|
|
119
|
+
return typeof fallback === 'function' ? fallback(error, this.reset) : fallback;
|
|
120
|
+
}
|
|
121
|
+
return /*#__PURE__*/_jsx(DefaultFallback, {
|
|
122
|
+
error: error,
|
|
123
|
+
reset: this.reset,
|
|
124
|
+
title: title,
|
|
125
|
+
message: message,
|
|
126
|
+
testID: testID
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=ErrorBoundary.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns a stable callback for surfacing an error caught outside React's
|
|
7
|
+
* render/commit cycle (an async callback, a rejected promise, a native event
|
|
8
|
+
* handler) to the nearest ErrorBoundary. React only catches synchronous
|
|
9
|
+
* render errors on its own; re-throwing during a state update is the
|
|
10
|
+
* documented way to route an async error into that same path.
|
|
11
|
+
*/
|
|
12
|
+
export const useErrorHandler = () => {
|
|
13
|
+
const [, setState] = useState();
|
|
14
|
+
return useCallback(error => {
|
|
15
|
+
setState(() => {
|
|
16
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
17
|
+
});
|
|
18
|
+
}, []);
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=useErrorHandler.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { ErrorBoundary } from "./ErrorBoundary.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wraps a component in an `ErrorBoundary`. Prefer this over hand-wrapping a
|
|
8
|
+
* screen at every call site when the same fallback/reset behaviour applies
|
|
9
|
+
* everywhere that component is rendered.
|
|
10
|
+
*/
|
|
11
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
12
|
+
export const withErrorBoundary = (Component, boundaryProps) => {
|
|
13
|
+
const displayName = Component.displayName ?? Component.name ?? 'Component';
|
|
14
|
+
const Wrapped = props => /*#__PURE__*/_jsx(ErrorBoundary, {
|
|
15
|
+
...boundaryProps,
|
|
16
|
+
children: /*#__PURE__*/_jsx(Component, {
|
|
17
|
+
...props
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
Wrapped.displayName = `withErrorBoundary(${displayName})`;
|
|
21
|
+
return Wrapped;
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=withErrorBoundary.js.map
|
|
@@ -604,7 +604,7 @@ const FilePicker = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
604
604
|
title: labels.sheetTitle,
|
|
605
605
|
options: sources,
|
|
606
606
|
labelOf: s => s.label,
|
|
607
|
-
|
|
607
|
+
getValue: s => s.key,
|
|
608
608
|
disabledOf: () => false,
|
|
609
609
|
descriptionOf: s => s.description,
|
|
610
610
|
iconOf: s => s.icon,
|
|
@@ -390,7 +390,20 @@ const FloatingActionButtonGroup = props => {
|
|
|
390
390
|
importantForAccessibility: "no",
|
|
391
391
|
testID: testID ? `${testID}-backdrop` : undefined
|
|
392
392
|
})
|
|
393
|
-
}) : null, /*#__PURE__*/_jsxs(View
|
|
393
|
+
}) : null, /*#__PURE__*/_jsxs(View
|
|
394
|
+
// The container is absolutely positioned and sized to the primary FAB,
|
|
395
|
+
// while the secondary actions sit ABOVE it via negative translateY, so
|
|
396
|
+
// they render outside their parent's bounds. Without box-none the
|
|
397
|
+
// container is itself a touch target across its box, and on Android a
|
|
398
|
+
// child drawn outside the parent's bounds does not receive touches at
|
|
399
|
+
// all. box-none makes the container transparent to touches while its
|
|
400
|
+
// children stay interactive, which is what an expanded menu needs.
|
|
401
|
+
//
|
|
402
|
+
// A consumer app carried this as a patch-package fix against two
|
|
403
|
+
// published versions rather than it landing here, which is how the
|
|
404
|
+
// library kept shipping the bug.
|
|
405
|
+
, {
|
|
406
|
+
pointerEvents: "box-none",
|
|
394
407
|
style: [positionStyle, containerStyle],
|
|
395
408
|
accessibilityRole: 'menu',
|
|
396
409
|
testID: testID,
|
|
@@ -392,6 +392,15 @@ const ZoomableImage = ({
|
|
|
392
392
|
savedScale.value = minScale;
|
|
393
393
|
savedTranslateX.value = 0;
|
|
394
394
|
savedTranslateY.value = 0;
|
|
395
|
+
// Mutation-tested equivalent under the jest reanimated mock (withSpring
|
|
396
|
+
// resolves synchronously to its target there): with this return removed,
|
|
397
|
+
// execution falls into `scale.value > maxScale` (always false, since
|
|
398
|
+
// minScale <= maxScale) and then `savedScale.value = scale.value`, which
|
|
399
|
+
// reads back the minScale just assigned above — same final state, so no
|
|
400
|
+
// jest assertion can distinguish the two. On a real device withSpring is
|
|
401
|
+
// asynchronous, so that fallthrough read could observe a stale
|
|
402
|
+
// mid-animation scale instead; kept as an explicit early return for that
|
|
403
|
+
// reason even though it isn't independently testable here.
|
|
395
404
|
return;
|
|
396
405
|
}
|
|
397
406
|
if (scale.value > maxScale) {
|
|
@@ -554,6 +563,10 @@ const buildLightboxStyles = theme => StyleSheet.create({
|
|
|
554
563
|
paddingHorizontal: 24
|
|
555
564
|
},
|
|
556
565
|
captionText: {
|
|
566
|
+
// Fixed white, not a theme token, and deliberately so: the lightbox is an
|
|
567
|
+
// always-dark surface (#000 backdrop with rgba scrims above), so it does
|
|
568
|
+
// not follow the colour scheme. `text.primary` would be dark ink on black
|
|
569
|
+
// in light mode. Do not "themify" this.
|
|
557
570
|
color: '#FFFFFF',
|
|
558
571
|
fontSize: 14,
|
|
559
572
|
lineHeight: 20,
|
|
@@ -145,7 +145,7 @@ const InlineSelect = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
145
145
|
accessibilityLabel: label ?? placeholder ?? 'Options',
|
|
146
146
|
options: options,
|
|
147
147
|
labelOf: labelOf,
|
|
148
|
-
|
|
148
|
+
getValue: valueOf,
|
|
149
149
|
disabledOf: disabledOf,
|
|
150
150
|
descriptionOf: descriptionOf,
|
|
151
151
|
iconOf: iconOf,
|
|
@@ -110,6 +110,12 @@ const Input = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
110
110
|
const shakeOnError = theme.components.input?.shakeOnError ?? false;
|
|
111
111
|
const prevErrorRef = useRef(hasError);
|
|
112
112
|
useEffect(() => {
|
|
113
|
+
// Equivalent-mutant note: removing this `return` cannot change behaviour.
|
|
114
|
+
// The line above it always sets `prevErrorRef.current = hasError` first,
|
|
115
|
+
// so falling through to the check below becomes `hasError &&
|
|
116
|
+
// !prevErrorRef.current` === `hasError && !hasError`, which is always
|
|
117
|
+
// false — the shake/haptic branch can never fire, and the redundant
|
|
118
|
+
// `prevErrorRef.current = hasError` reassignment at the bottom is a no-op.
|
|
113
119
|
if (!shakeOnError) {
|
|
114
120
|
prevErrorRef.current = hasError;
|
|
115
121
|
return;
|