@lowdefy/shell 0.0.0-experimental-20261019052246

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +308 -0
  3. package/app/(t0,t1,t2,t3,t4)/[...path].js +38 -0
  4. package/app/(t0,t1,t2,t3,t4)/_layout.js +48 -0
  5. package/app/(t0,t1,t2,t3,t4)/index.js +38 -0
  6. package/app/+native-intent.js +30 -0
  7. package/app/_layout.js +33 -0
  8. package/app.config.ts +168 -0
  9. package/applyShellPlugins.js +43 -0
  10. package/authCallbackIntentFilters.js +46 -0
  11. package/babel.config.js +25 -0
  12. package/checkShellConfig.js +145 -0
  13. package/checkShellPlugins.js +74 -0
  14. package/getPrivacyManifest.js +39 -0
  15. package/metro.config.js +73 -0
  16. package/native-fingerprint.json +12 -0
  17. package/package.json +130 -0
  18. package/profiles.js +274 -0
  19. package/readShellConfig.js +36 -0
  20. package/resolveShellPlugins.js +109 -0
  21. package/runtimeVersion.js +105 -0
  22. package/scripts/checkReleaseBundle.mjs +358 -0
  23. package/scripts/createDayjsLocales.mjs +67 -0
  24. package/scripts/createShellAssets.mjs +54 -0
  25. package/scripts/createShellVersion.mjs +83 -0
  26. package/scripts/createTypeManifest.mjs +205 -0
  27. package/shell.dev.json +11 -0
  28. package/shellVariant.js +111 -0
  29. package/src/ShellContext.js +32 -0
  30. package/src/actions/CopyToClipboard.js +29 -0
  31. package/src/actions/CopyToClipboard.test.js +33 -0
  32. package/src/actions/GeolocationCurrentPosition.js +55 -0
  33. package/src/actions/GeolocationCurrentPosition.test.js +66 -0
  34. package/src/actions/GetLocalStorage.js +36 -0
  35. package/src/actions/RemoveLocalStorage.js +31 -0
  36. package/src/actions/ScrollTo.js +66 -0
  37. package/src/actions/ScrollTo.test.js +83 -0
  38. package/src/actions/SetFocus.js +33 -0
  39. package/src/actions/SetFocus.test.js +49 -0
  40. package/src/actions/SetLocalStorage.js +34 -0
  41. package/src/actions/localStorage.test.js +68 -0
  42. package/src/actions/measureInScrollView.js +36 -0
  43. package/src/actions/nativeActions.js +41 -0
  44. package/src/auth/AuthButton.js +44 -0
  45. package/src/auth/AuthGate.js +57 -0
  46. package/src/auth/AuthLink.js +43 -0
  47. package/src/auth/LockScreen.js +67 -0
  48. package/src/auth/LockScreen.test.js +37 -0
  49. package/src/auth/SignInScreen.js +256 -0
  50. package/src/auth/SignInScreen.test.js +182 -0
  51. package/src/auth/createAppAuth.js +306 -0
  52. package/src/auth/createAppAuth.test.js +414 -0
  53. package/src/auth/createAuthApi.js +99 -0
  54. package/src/auth/createAuthApi.test.js +135 -0
  55. package/src/auth/createCoreAuth.js +96 -0
  56. package/src/auth/createCoreAuth.test.js +188 -0
  57. package/src/auth/createSessionStores.js +45 -0
  58. package/src/auth/createTokenStore.js +103 -0
  59. package/src/auth/createTokenStore.test.js +116 -0
  60. package/src/auth/getAuthSettings.js +24 -0
  61. package/src/auth/getAuthView.js +39 -0
  62. package/src/auth/getAuthView.test.js +52 -0
  63. package/src/auth/getSheetReturnUrl.js +35 -0
  64. package/src/auth/getTokenStoreOptions.js +46 -0
  65. package/src/auth/isBackgroundShell.js +26 -0
  66. package/src/auth/isSheetReturnLink.js +35 -0
  67. package/src/auth/parseQuery.js +39 -0
  68. package/src/auth/purgeAppData.js +45 -0
  69. package/src/auth/shouldLock.js +26 -0
  70. package/src/auth/signInWithSheet.js +97 -0
  71. package/src/auth/signInWithSheet.test.js +205 -0
  72. package/src/auth/styles.js +78 -0
  73. package/src/auth/unregisterPushToken.js +21 -0
  74. package/src/auth/usesBiometricUnlock.js +23 -0
  75. package/src/auth/watchSession.js +47 -0
  76. package/src/auth/withoutCookies.js +26 -0
  77. package/src/cache/createSqliteCacheStore.js +113 -0
  78. package/src/cache/createSqliteCacheStore.test.js +93 -0
  79. package/src/cache/getCacheKey.js +25 -0
  80. package/src/cache/getLastCacheKeyName.js +22 -0
  81. package/src/components/AppProvider.js +90 -0
  82. package/src/components/Frame.js +116 -0
  83. package/src/components/Frame.test.js +101 -0
  84. package/src/components/LoadingScreen.js +35 -0
  85. package/src/components/MenuButton.js +108 -0
  86. package/src/components/MenuButton.test.js +51 -0
  87. package/src/components/MessageScreen.js +51 -0
  88. package/src/components/NavigationTheme.js +33 -0
  89. package/src/components/NoConnectionBanner.js +51 -0
  90. package/src/components/NoConnectionBanner.test.js +73 -0
  91. package/src/components/OtherPageScreen.js +56 -0
  92. package/src/components/PageScreen.js +244 -0
  93. package/src/components/PageScreen.test.js +492 -0
  94. package/src/components/ShellRoot.js +34 -0
  95. package/src/components/ShellRoot.test.js +62 -0
  96. package/src/components/ShellThemeProvider.js +45 -0
  97. package/src/components/SwitchToTab.js +36 -0
  98. package/src/components/SwitchToTab.test.js +63 -0
  99. package/src/components/TabIconImages.js +72 -0
  100. package/src/components/UpdateScreen.js +36 -0
  101. package/src/components/getNavigationTheme.js +44 -0
  102. package/src/components/getNavigationTheme.test.js +41 -0
  103. package/src/components/styles.js +50 -0
  104. package/src/components/useSurfaceTokens.js +47 -0
  105. package/src/config/getAppApiBase.js +31 -0
  106. package/src/config/getShellConfig.js +35 -0
  107. package/src/config/getShellConfig.test.js +80 -0
  108. package/src/createShellApp.js +105 -0
  109. package/src/dev/DevShell.js +228 -0
  110. package/src/dev/DevShell.test.js +233 -0
  111. package/src/dev/connect/ConfirmSheet.js +71 -0
  112. package/src/dev/connect/ConnectScreen.js +117 -0
  113. package/src/dev/connect/QrScanner.js +89 -0
  114. package/src/dev/connection/createDevShellConfig.js +47 -0
  115. package/src/dev/connection/createDevTransport.js +51 -0
  116. package/src/dev/connection/createDevTransport.test.js +87 -0
  117. package/src/dev/connection/createServerList.js +48 -0
  118. package/src/dev/connection/isLoopbackHost.js +25 -0
  119. package/src/dev/connection/pairedShellApp.test.js +104 -0
  120. package/src/dev/connection/parseDevLink.js +66 -0
  121. package/src/dev/connection/parseDevLink.test.js +97 -0
  122. package/src/dev/connection/parseQuery.js +33 -0
  123. package/src/dev/connection/parseServerInput.js +40 -0
  124. package/src/dev/connection/parseServerUrl.js +45 -0
  125. package/src/dev/devStyles.js +97 -0
  126. package/src/dev/index.js +21 -0
  127. package/src/dev/inject/createDeviceInjections.js +185 -0
  128. package/src/dev/inject/createDeviceInjections.test.js +208 -0
  129. package/src/dev/inject/createOfflineFetch.js +35 -0
  130. package/src/dev/inject/createOfflineFetch.test.js +68 -0
  131. package/src/dev/menu/BlockGallery.js +33 -0
  132. package/src/dev/menu/DevMenu.js +121 -0
  133. package/src/dev/menu/createLogBuffer.js +83 -0
  134. package/src/dev/menu/watchShake.js +52 -0
  135. package/src/dev/menu/watchShake.test.js +61 -0
  136. package/src/dev/recorder/createNativeRecorder.js +204 -0
  137. package/src/dev/recorder/createNativeRecorder.test.js +307 -0
  138. package/src/dev/redirectDevLink.js +30 -0
  139. package/src/dev/redirectDevLink.test.js +49 -0
  140. package/src/dev/steps/createStepRunner.js +184 -0
  141. package/src/dev/steps/createStepRunner.test.js +262 -0
  142. package/src/dev/steps/findBlock.js +78 -0
  143. package/src/dev/steps/isContextSettled.js +37 -0
  144. package/src/dev/steps/waitForSettle.js +39 -0
  145. package/src/dev/steps/waitUntil.js +40 -0
  146. package/src/dev/tab/DevTabClient.js +97 -0
  147. package/src/dev/tab/answerEval.js +39 -0
  148. package/src/dev/tab/answerInspect.js +45 -0
  149. package/src/dev/tab/answerLoadState.js +50 -0
  150. package/src/dev/tab/createDevTabClient.js +277 -0
  151. package/src/dev/tab/createDevTabClient.test.js +314 -0
  152. package/src/dev/tab/devClientMarker.js +22 -0
  153. package/src/dev/tab/devClientMarker.test.js +23 -0
  154. package/src/dev/tab/findPageContext.js +43 -0
  155. package/src/dev/tab/getShownPage.js +42 -0
  156. package/src/dev/tab/readEventStream.js +68 -0
  157. package/src/dev/tab/reloadApp.js +42 -0
  158. package/src/dev/tab/reloadApp.test.js +115 -0
  159. package/src/device/injected.js +60 -0
  160. package/src/device/injected.test.js +39 -0
  161. package/src/host/createDeviceStore.js +32 -0
  162. package/src/host/createMemoryStore.js +32 -0
  163. package/src/host/createNetworkStatus.js +94 -0
  164. package/src/host/createNetworkStatus.test.js +94 -0
  165. package/src/host/createRouterAdapter.js +33 -0
  166. package/src/host/createShellFetch.js +60 -0
  167. package/src/host/createShellFetch.test.js +150 -0
  168. package/src/host/createShellGlobals.js +62 -0
  169. package/src/host/createShellHost.js +73 -0
  170. package/src/host/createShellHost.test.js +36 -0
  171. package/src/host/getOrigin.js +26 -0
  172. package/src/host/isOnline.js +22 -0
  173. package/src/host/networkStatus.js +24 -0
  174. package/src/host/withShellHeader.js +25 -0
  175. package/src/host/withShellHeader.test.js +28 -0
  176. package/src/js/evaluateJsEntries.js +43 -0
  177. package/src/js/evaluateJsEntries.test.js +48 -0
  178. package/src/locale/createShellLocale.js +112 -0
  179. package/src/locale/createShellLocale.test.js +74 -0
  180. package/src/locale/dayjsLocales.js +165 -0
  181. package/src/locale/getDeviceLocales.js +23 -0
  182. package/src/locale/loadDayjsLocale.js +32 -0
  183. package/src/locale/resolveActiveLocale.js +46 -0
  184. package/src/locale/resolveActiveLocale.test.js +40 -0
  185. package/src/navigation/SlotContext.js +24 -0
  186. package/src/navigation/frameSlots.js +27 -0
  187. package/src/navigation/getFrame.js +30 -0
  188. package/src/navigation/getInitialScreenOptions.js +63 -0
  189. package/src/navigation/getInitialScreenOptions.test.js +66 -0
  190. package/src/navigation/getPresentation.js +28 -0
  191. package/src/navigation/getSlotRootPath.js +27 -0
  192. package/src/navigation/getStackOptions.js +185 -0
  193. package/src/navigation/getTabHref.js +45 -0
  194. package/src/navigation/getTabIcons.js +43 -0
  195. package/src/navigation/isPageUnderHeader.js +46 -0
  196. package/src/navigation/navigation.test.js +342 -0
  197. package/src/navigation/toRouterHref.js +29 -0
  198. package/src/plugins/appTypes.js +28 -0
  199. package/src/plugins/prefixTypes.js +28 -0
  200. package/src/plugins/shellPlugins.json +14 -0
  201. package/src/plugins/typeManifest.js +649 -0
  202. package/src/plugins/types.js +61 -0
  203. package/src/plugins/types.test.js +173 -0
  204. package/src/polyfills/installGetRandomValues.js +28 -0
  205. package/src/session/createAppSession.js +318 -0
  206. package/src/session/createAppSession.test.js +359 -0
  207. package/src/session/getPageView.js +59 -0
  208. package/src/session/getPageView.test.js +137 -0
  209. package/src/sync/encodePagePath.js +27 -0
  210. package/src/sync/fetchPage.js +49 -0
  211. package/src/sync/fetchRoot.js +38 -0
  212. package/src/sync/fetchUser.js +28 -0
  213. package/src/sync/runLimited.js +36 -0
  214. package/src/sync/toRootConfig.js +35 -0
  215. package/src/test/createFakeNetwork.js +40 -0
  216. package/src/test/createFakeSecureStore.js +40 -0
  217. package/src/test/createFakeServer.js +98 -0
  218. package/src/test/createMemoryCacheStore.js +59 -0
  219. package/src/test/expoImage.js +38 -0
  220. package/src/test/expoSpeechRecognition.js +31 -0
  221. package/src/test/expoUi.js +55 -0
  222. package/src/test/expoUiBottomSheet.js +64 -0
  223. package/src/test/expoUiDateTimePicker.js +26 -0
  224. package/src/test/expoUiMenu.js +46 -0
  225. package/src/test/expoUiSegmentedControl.js +47 -0
  226. package/src/test/expoUiSwiftUi.js +62 -0
  227. package/src/test/expoUiSwiftUiModifiers.js +23 -0
  228. package/src/test/expoWebBrowser.js +23 -0
  229. package/src/test/fixtures.js +98 -0
  230. package/src/test/renderPageScreen.js +109 -0
  231. package/src/types/checkTypeSet.js +39 -0
  232. package/src/types/collectPageTypes.js +49 -0
  233. package/src/types/collectPageTypes.test.js +90 -0
  234. package/src/types/findMissingPageTypes.js +40 -0
  235. package/src/version/compareVersions.js +38 -0
  236. package/src/version/isShellTooOld.js +30 -0
  237. package/src/version/shellVersion.js +21 -0
  238. package/src/version/shellVersion.test.js +32 -0
  239. package/withExcludedNativeModules.js +93 -0
@@ -0,0 +1,28 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // GET /api/user: the caller the pages render (the _user operator, and the caller the core checks
18
+ // each response's stamp against).
19
+ async function fetchUser({ apiBase, fetchWithAuth }) {
20
+ const res = await fetchWithAuth(`${apiBase}/api/user`, { method: 'GET', headers: {} });
21
+ if (!res.ok) {
22
+ throw new Error(`The app's server answered ${res.status} to the user request.`);
23
+ }
24
+ const body = await res.json();
25
+ return body.user ?? null;
26
+ }
27
+
28
+ export default fetchUser;
@@ -0,0 +1,36 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // Runs fn over every item, at most `limit` at a time. Resolves once all have settled; a failed
18
+ // item does not stop the others, and the failures are returned.
19
+ async function runLimited({ items, limit, fn }) {
20
+ const queue = [...items];
21
+ const failures = [];
22
+ async function worker() {
23
+ while (queue.length > 0) {
24
+ const item = queue.shift();
25
+ try {
26
+ await fn(item);
27
+ } catch (error) {
28
+ failures.push({ item, error });
29
+ }
30
+ }
31
+ }
32
+ await Promise.all(Array.from({ length: Math.min(limit, queue.length) }, () => worker()));
33
+ return failures;
34
+ }
35
+
36
+ export default runLimited;
@@ -0,0 +1,35 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // The root config @lowdefy/client-native renders pages with, from the mobile root. The web menus
18
+ // (_menu) have no mobile counterpart; the mobile frame is the shell's. The theme
19
+ // ({ darkMode, light, dark }) and the Tailwind class map style the blocks. A root cached from a
20
+ // server built before they were served has no class map or i18n.
21
+ function toRootConfig(root) {
22
+ return {
23
+ classMap: root.classMap ?? {},
24
+ events: root.events,
25
+ home: { configured: true, pageId: root.homePageId },
26
+ i18n: root.i18n ?? {},
27
+ lowdefyApp: root.lowdefyApp ?? {},
28
+ lowdefyGlobal: root.lowdefyGlobal ?? {},
29
+ menus: [],
30
+ pagePaths: Object.fromEntries(root.pageIndex.map((entry) => [entry.pageId, entry.path])),
31
+ theme: root.theme ?? {},
32
+ };
33
+ }
34
+
35
+ export default toRootConfig;
@@ -0,0 +1,40 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ const ONLINE = { type: 'WIFI', isConnected: true, isInternetReachable: true };
18
+
19
+ // expo-network as createNetworkStatus reads it, with the phone's network state set by the test.
20
+ function createFakeNetwork({ state: initial = ONLINE } = {}) {
21
+ const listeners = new Set();
22
+ let state = initial;
23
+ return {
24
+ addNetworkStateListener(listener) {
25
+ listeners.add(listener);
26
+ return { remove: () => listeners.delete(listener) };
27
+ },
28
+ getNetworkStateAsync: async () => state,
29
+ // The phone's network changes, as expo-network reports it.
30
+ change(next) {
31
+ state = next;
32
+ listeners.forEach((listener) => listener(next));
33
+ },
34
+ listening: () => listeners.size > 0,
35
+ };
36
+ }
37
+
38
+ export const OFFLINE = { type: 'NONE', isConnected: false, isInternetReachable: false };
39
+ export { ONLINE };
40
+ export default createFakeNetwork;
@@ -0,0 +1,40 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { jest } from '@jest/globals';
18
+
19
+ // expo-secure-store over a Map, keyed as the native module keys items: by key and by whether the
20
+ // item is behind biometrics (a protected item is a different Keychain service on iOS).
21
+ function createFakeSecureStore() {
22
+ const items = new Map();
23
+ function itemKey(key, options = {}) {
24
+ return `${key}|${options.requireAuthentication === true ? 'auth' : 'no-auth'}`;
25
+ }
26
+ return {
27
+ items,
28
+ AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: 'AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY',
29
+ WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'WHEN_UNLOCKED_THIS_DEVICE_ONLY',
30
+ getItemAsync: jest.fn(async (key, options) => items.get(itemKey(key, options))?.value ?? null),
31
+ setItemAsync: jest.fn(async (key, value, options) => {
32
+ items.set(itemKey(key, options), { value, options });
33
+ }),
34
+ deleteItemAsync: jest.fn(async (key, options) => {
35
+ items.delete(itemKey(key, options));
36
+ }),
37
+ };
38
+ }
39
+
40
+ export default createFakeSecureStore;
@@ -0,0 +1,98 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { jest } from '@jest/globals';
18
+
19
+ function createResponse({ body = null, headers = {}, status = 200 }) {
20
+ const lowerHeaders = Object.fromEntries(
21
+ Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value])
22
+ );
23
+ return {
24
+ ok: status >= 200 && status < 300,
25
+ status,
26
+ headers: { get: (name) => lowerHeaders[name.toLowerCase()] ?? null },
27
+ json: async () => structuredClone(body),
28
+ };
29
+ }
30
+
31
+ function header(init, name) {
32
+ const entry = Object.entries(init?.headers ?? {}).find(
33
+ ([key]) => key.toLowerCase() === name.toLowerCase()
34
+ );
35
+ return entry?.[1] ?? null;
36
+ }
37
+
38
+ /**
39
+ * A Lowdefy server for the shell's tests. `state` holds what it serves and may be changed between
40
+ * calls: `caller` (the x-lowdefy-caller stamp), `user`, `root` and its `rootEtag`, `pages`
41
+ * ({ [path]: { pageConfig, etag } }), `buildId`, `requests` (a function answering
42
+ * /api/request/* calls) and `auth` (a function of { path, init } answering /api/auth/* calls with
43
+ * { body, headers, status }). A matching If-None-Match gets a 304. Every call is in `fetch.mock.calls`.
44
+ */
45
+ function createFakeServer(initial) {
46
+ const state = { buildId: 'build-1', caller: '', user: null, ...initial };
47
+ const fetch = jest.fn(async (url, init) => {
48
+ const { pathname } = new URL(url);
49
+ const callerHeaders = { 'x-lowdefy-caller': state.caller };
50
+ if (pathname === '/api/mobile/root') {
51
+ if (header(init, 'if-none-match') === state.rootEtag) {
52
+ return createResponse({ status: 304, headers: { ...callerHeaders, etag: state.rootEtag } });
53
+ }
54
+ return createResponse({
55
+ body: state.root,
56
+ headers: { ...callerHeaders, etag: state.rootEtag },
57
+ });
58
+ }
59
+ if (pathname === '/api/user') {
60
+ return createResponse({ body: { user: state.user } });
61
+ }
62
+ if (pathname.startsWith('/api/page/')) {
63
+ const path = decodeURIComponent(pathname.slice('/api/page/'.length));
64
+ const page = state.pages[path];
65
+ if (page === undefined) {
66
+ return createResponse({
67
+ status: 404,
68
+ body: state.otherSurfaces?.[path] ?? { pageConfig: null },
69
+ });
70
+ }
71
+ if (header(init, 'if-none-match') === page.etag) {
72
+ return createResponse({ status: 304, headers: { ...callerHeaders, etag: page.etag } });
73
+ }
74
+ return createResponse({
75
+ body: { buildId: state.buildId, pageConfig: page.pageConfig },
76
+ headers: { ...callerHeaders, etag: page.etag },
77
+ });
78
+ }
79
+ if (pathname.startsWith('/api/request/')) {
80
+ const build = header(init, 'x-lowdefy-build');
81
+ if (build !== null && build !== state.buildId) {
82
+ return createResponse({
83
+ status: 409,
84
+ body: { name: 'UserError', message: 'Other build.', buildId: state.buildId },
85
+ });
86
+ }
87
+ return createResponse({ body: state.requests?.(url, init) ?? { success: true } });
88
+ }
89
+ if (pathname.startsWith('/api/auth/') && state.auth !== undefined) {
90
+ return createResponse(state.auth({ path: pathname.slice('/api/auth'.length), init }));
91
+ }
92
+ return createResponse({ status: 404, body: {} });
93
+ });
94
+ return { fetch, state };
95
+ }
96
+
97
+ export { header };
98
+ export default createFakeServer;
@@ -0,0 +1,59 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // The page cache held in memory, with the same methods as createSqliteCacheStore.
18
+ function createMemoryCacheStore() {
19
+ const roots = new Map();
20
+ const pages = new Map();
21
+ function pagesOf(cacheKey) {
22
+ if (!pages.has(cacheKey)) pages.set(cacheKey, new Map());
23
+ return pages.get(cacheKey);
24
+ }
25
+ return {
26
+ roots,
27
+ pages,
28
+ async read(cacheKey) {
29
+ if (!roots.has(cacheKey)) return null;
30
+ return {
31
+ ...structuredClone(roots.get(cacheKey)),
32
+ pages: Object.fromEntries(structuredClone([...pagesOf(cacheKey).entries()])),
33
+ };
34
+ },
35
+ async writeRoot({ cacheKey, entry }) {
36
+ roots.set(cacheKey, structuredClone(entry));
37
+ },
38
+ async writePage({ cacheKey, pageId, page }) {
39
+ pagesOf(cacheKey).set(pageId, structuredClone(page));
40
+ },
41
+ async removeEntry({ cacheKey }) {
42
+ roots.delete(cacheKey);
43
+ pages.delete(cacheKey);
44
+ },
45
+ async removeApp({ appId }) {
46
+ [...roots.keys(), ...pages.keys()]
47
+ .filter((cacheKey) => cacheKey.startsWith(`${appId}:`))
48
+ .forEach((cacheKey) => {
49
+ roots.delete(cacheKey);
50
+ pages.delete(cacheKey);
51
+ });
52
+ },
53
+ async removePages({ cacheKey, pageIds }) {
54
+ pageIds.forEach((pageId) => pagesOf(cacheKey).delete(pageId));
55
+ },
56
+ };
57
+ }
58
+
59
+ export default createMemoryCacheStore;
@@ -0,0 +1,38 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import { View } from 'react-native';
19
+
20
+ // A stand-in for expo-image's Image, whose view needs a native module Jest does not have. It keeps
21
+ // its props, so a test reads the source and fires the load and error events:
22
+ // fireEvent(image, 'load', { cacheType: 'none' }) or fireEvent(image, 'error', { error: '' }).
23
+ function Image({ cachePolicy, contentFit, onError, onLoad, recyclingKey, source, style, testID }) {
24
+ return (
25
+ <View
26
+ cachePolicy={cachePolicy}
27
+ contentFit={contentFit}
28
+ onError={onError}
29
+ onLoad={onLoad}
30
+ recyclingKey={recyclingKey}
31
+ source={source}
32
+ style={style}
33
+ testID={testID ?? 'expo-image'}
34
+ />
35
+ );
36
+ }
37
+
38
+ export { Image };
@@ -0,0 +1,31 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // A stand-in for expo-speech-recognition, whose recogniser is a native module Jest does not have.
18
+ // The native blocks' dictation hook asks it whether the phone can recognise speech; here it
19
+ // cannot, so screens render without a mic. blocks-native's own tests drive a fuller stand-in.
20
+ const ExpoSpeechRecognitionModule = {
21
+ abort: () => {},
22
+ addListener: () => ({ remove: () => {} }),
23
+ getSupportedLocales: async () => ({ locales: [], installedLocales: [] }),
24
+ isRecognitionAvailable: () => false,
25
+ requestPermissionsAsync: async () => ({ granted: false, status: 'denied' }),
26
+ start: () => {},
27
+ stop: () => {},
28
+ supportsOnDeviceRecognition: () => false,
29
+ };
30
+
31
+ export { ExpoSpeechRecognitionModule };
@@ -0,0 +1,55 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import { Text, View } from 'react-native';
19
+
20
+ // Stand-ins for the @expo/ui controls the native blocks use: @expo/ui draws SwiftUI and Jetpack
21
+ // Compose views through native modules Jest does not have. They keep the props the blocks pass,
22
+ // so a test fires their callbacks with fireEvent(view, 'valueChange', ...).
23
+ function Host({ children }) {
24
+ return <View>{children}</View>;
25
+ }
26
+
27
+ function Picker({ children, onValueChange, selectedValue, testID }) {
28
+ const items = React.Children.toArray(children).map((child) => child.props);
29
+ return (
30
+ <View items={items} onValueChange={onValueChange} selectedValue={selectedValue} testID={testID}>
31
+ {items.map((item) => (
32
+ <Text key={String(item.value)}>{item.label}</Text>
33
+ ))}
34
+ </View>
35
+ );
36
+ }
37
+ Picker.Item = () => null;
38
+
39
+ function Switch({ onValueChange, testID, value }) {
40
+ return <View onValueChange={onValueChange} testID={testID} value={value} />;
41
+ }
42
+
43
+ function Icon({ color, modifiers, name, size }) {
44
+ return (
45
+ <View
46
+ color={color}
47
+ modifiers={modifiers}
48
+ name={name}
49
+ size={size}
50
+ testID={`expo-ui-icon-${name}`}
51
+ />
52
+ );
53
+ }
54
+
55
+ export { Host, Icon, Picker, Switch };
@@ -0,0 +1,64 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React, { useEffect, useRef } from 'react';
18
+ import { View } from 'react-native';
19
+
20
+ // A stand-in for @expo/ui's community BottomSheet. It draws its children while `index` is 0 or
21
+ // more, as the native sheet presents them, and keeps its props. As the native sheet, it calls
22
+ // onClose once per dismissal: when index turns -1, or when a test swipes it down with
23
+ // fireEvent(view, 'swipeDismiss').
24
+ function BottomSheet({
25
+ backgroundStyle,
26
+ children,
27
+ enableDynamicSizing,
28
+ enablePanDownToClose,
29
+ handleComponent,
30
+ index,
31
+ onClose,
32
+ snapPoints,
33
+ }) {
34
+ const closed = useRef(index < 0);
35
+ function fireClose() {
36
+ if (closed.current) return;
37
+ closed.current = true;
38
+ onClose?.();
39
+ }
40
+ useEffect(() => {
41
+ if (index < 0) {
42
+ fireClose();
43
+ } else {
44
+ closed.current = false;
45
+ }
46
+ });
47
+ return (
48
+ <View
49
+ backgroundStyle={backgroundStyle}
50
+ enableDynamicSizing={enableDynamicSizing}
51
+ enablePanDownToClose={enablePanDownToClose}
52
+ handleComponent={handleComponent}
53
+ index={index}
54
+ onSwipeDismiss={fireClose}
55
+ snapPoints={snapPoints}
56
+ testID="expo-ui-bottom-sheet"
57
+ >
58
+ {index >= 0 ? children : null}
59
+ </View>
60
+ );
61
+ }
62
+
63
+ export { BottomSheet };
64
+ export default BottomSheet;
@@ -0,0 +1,26 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import { View } from 'react-native';
19
+
20
+ // A stand-in for @expo/ui's community DateTimePicker that keeps the props DateSelector passes.
21
+ function DateTimePicker({ onValueChange, testID, value }) {
22
+ return <View onValueChange={onValueChange} testID={testID} value={value} />;
23
+ }
24
+
25
+ export { DateTimePicker };
26
+ export default DateTimePicker;
@@ -0,0 +1,46 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import { View } from 'react-native';
19
+
20
+ // A stand-in for @expo/ui's community MenuView that keeps its props, so a test reads the actions
21
+ // it was given and picks one with fireEvent(view, 'pressAction', { nativeEvent: { event: id } }).
22
+ function MenuView({
23
+ actions,
24
+ children,
25
+ onPressAction,
26
+ shouldOpenOnLongPress,
27
+ style,
28
+ testID,
29
+ title,
30
+ }) {
31
+ return (
32
+ <View
33
+ actions={actions}
34
+ menuTitle={title}
35
+ onPressAction={onPressAction}
36
+ shouldOpenOnLongPress={shouldOpenOnLongPress}
37
+ style={style}
38
+ testID={testID}
39
+ >
40
+ {children}
41
+ </View>
42
+ );
43
+ }
44
+
45
+ export { MenuView };
46
+ export default MenuView;
@@ -0,0 +1,47 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import { View } from 'react-native';
19
+
20
+ // A stand-in for @expo/ui's community SegmentedControl that keeps its props, so a test picks a
21
+ // segment with fireEvent(view, 'change', { nativeEvent: { selectedSegmentIndex, value } }).
22
+ function SegmentedControl({
23
+ appearance,
24
+ enabled = true,
25
+ onChange,
26
+ selectedIndex,
27
+ style,
28
+ testID,
29
+ tintColor,
30
+ values,
31
+ }) {
32
+ return (
33
+ <View
34
+ appearance={appearance}
35
+ enabled={enabled}
36
+ onChange={onChange}
37
+ selectedIndex={selectedIndex}
38
+ style={style}
39
+ testID={testID}
40
+ tintColor={tintColor}
41
+ values={values}
42
+ />
43
+ );
44
+ }
45
+
46
+ export { SegmentedControl };
47
+ export default SegmentedControl;