@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,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
+ import React from 'react';
18
+ import { ActivityIndicator, View } from 'react-native';
19
+
20
+ import styles from './styles.js';
21
+ import useSurfaceTokens from './useSurfaceTokens.js';
22
+
23
+ function LoadingScreen() {
24
+ const tokens = useSurfaceTokens();
25
+ return (
26
+ <View
27
+ style={[styles.centered, { backgroundColor: tokens.colorGroupedBackground }]}
28
+ testID="shell-loading"
29
+ >
30
+ <ActivityIndicator color={tokens.colorSecondaryLabel} />
31
+ </View>
32
+ );
33
+ }
34
+
35
+ export default LoadingScreen;
@@ -0,0 +1,108 @@
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, { useState } from 'react';
18
+ import { Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
19
+ import { router } from 'expo-router';
20
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
21
+ import { Icon } from '@lowdefy/client-native';
22
+
23
+ import { useSessionState } from '../ShellContext.js';
24
+
25
+ const styles = StyleSheet.create({
26
+ button: {
27
+ paddingHorizontal: 8,
28
+ paddingVertical: 4,
29
+ },
30
+ buttonText: {
31
+ fontSize: 16,
32
+ },
33
+ backdrop: {
34
+ flex: 1,
35
+ flexDirection: 'row',
36
+ backgroundColor: 'rgba(0,0,0,0.3)',
37
+ },
38
+ sheet: {
39
+ width: '80%',
40
+ maxWidth: 360,
41
+ backgroundColor: '#ffffff',
42
+ },
43
+ item: {
44
+ alignItems: 'center',
45
+ columnGap: 16,
46
+ flexDirection: 'row',
47
+ paddingHorizontal: 24,
48
+ paddingVertical: 16,
49
+ },
50
+ itemText: {
51
+ fontSize: 17,
52
+ },
53
+ });
54
+
55
+ // The drawer frame: a header button that opens the app's drawer menu over the screen, each item
56
+ // with its icon drawn from the icon data the root carries. Picking an item replaces the stack with
57
+ // that page.
58
+ function MenuButton({ items }) {
59
+ const [open, setOpen] = useState(false);
60
+ const { root } = useSessionState();
61
+ const insets = useSafeAreaInsets();
62
+ function openPage(pageId) {
63
+ setOpen(false);
64
+ const path = root.pageIndex.find((entry) => entry.pageId === pageId)?.path;
65
+ if (router.canDismiss()) router.dismissAll();
66
+ router.replace(`/${path}`);
67
+ }
68
+ return (
69
+ <>
70
+ <Pressable
71
+ accessibilityLabel="Menu"
72
+ accessibilityRole="button"
73
+ onPress={() => setOpen(true)}
74
+ style={styles.button}
75
+ testID="shell-menu"
76
+ >
77
+ <Text style={styles.buttonText}>Menu</Text>
78
+ </Pressable>
79
+ <Modal animationType="fade" onRequestClose={() => setOpen(false)} transparent visible={open}>
80
+ <View style={styles.backdrop}>
81
+ <ScrollView style={[styles.sheet, { paddingTop: insets.top }]}>
82
+ {items.map((item) => (
83
+ <Pressable
84
+ accessibilityRole="button"
85
+ key={item.pageId}
86
+ onPress={() => openPage(item.pageId)}
87
+ style={styles.item}
88
+ testID={`menu-${item.pageId}`}
89
+ >
90
+ {item.iconData ? (
91
+ <Icon data={item.iconData} size={22} testID={`menu-${item.pageId}-icon`} />
92
+ ) : null}
93
+ <Text style={styles.itemText}>{item.title ?? item.pageId}</Text>
94
+ </Pressable>
95
+ ))}
96
+ </ScrollView>
97
+ <Pressable
98
+ accessibilityLabel="Close menu"
99
+ onPress={() => setOpen(false)}
100
+ style={{ flex: 1 }}
101
+ />
102
+ </View>
103
+ </Modal>
104
+ </>
105
+ );
106
+ }
107
+
108
+ export default MenuButton;
@@ -0,0 +1,51 @@
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 { jest } from '@jest/globals';
19
+ import { fireEvent, render, screen } from '@testing-library/react-native';
20
+
21
+ import MenuButton from './MenuButton.js';
22
+ import ShellContext from '../ShellContext.js';
23
+
24
+ jest.mock('expo-router', () => ({
25
+ router: { canDismiss: () => false, replace: jest.fn() },
26
+ }));
27
+
28
+ jest.mock('react-native-safe-area-context', () => ({
29
+ useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
30
+ }));
31
+
32
+ const house = { node: [['path', { d: 'M3 10a2 2 0 0 1 .709-1.528l7-6' }]] };
33
+
34
+ test('the drawer menu draws each item icon from the icon data the root carries', async () => {
35
+ const state = { root: { pageIndex: [{ pageId: 'today', path: 'today' }] } };
36
+ const session = { getState: () => state, subscribe: () => () => undefined };
37
+ await render(
38
+ <ShellContext.Provider value={{ session }}>
39
+ <MenuButton
40
+ items={[
41
+ { pageId: 'today', title: 'Today', icon: 'House', iconData: house },
42
+ { pageId: 'jobs', title: 'Jobs' },
43
+ ]}
44
+ />
45
+ </ShellContext.Provider>
46
+ );
47
+ fireEvent.press(screen.getByTestId('shell-menu'));
48
+ expect(await screen.findByTestId('menu-today-icon')).toBeTruthy();
49
+ expect(screen.queryByTestId('menu-jobs-icon')).toBeNull();
50
+ expect(screen.getByText('Jobs')).toBeTruthy();
51
+ });
@@ -0,0 +1,51 @@
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 { Pressable, Text, View } from 'react-native';
19
+
20
+ import styles from './styles.js';
21
+ import useSurfaceTokens from './useSurfaceTokens.js';
22
+
23
+ // A full-screen message with an optional action button: update, error and not found screens.
24
+ function MessageScreen({ action, body, testID, title }) {
25
+ const tokens = useSurfaceTokens();
26
+ return (
27
+ <View
28
+ style={[styles.centered, { backgroundColor: tokens.colorGroupedBackground }]}
29
+ testID={testID}
30
+ >
31
+ <Text style={[styles.title, { color: tokens.colorLabel }]}>{title}</Text>
32
+ {body ? (
33
+ <Text style={[styles.body, { color: tokens.colorSecondaryLabel }]}>{body}</Text>
34
+ ) : null}
35
+ {action ? (
36
+ <Pressable
37
+ accessibilityRole="button"
38
+ onPress={action.onPress}
39
+ style={({ pressed }) => [
40
+ styles.button,
41
+ { backgroundColor: tokens.colorTint, opacity: pressed ? 0.7 : 1 },
42
+ ]}
43
+ >
44
+ <Text style={styles.buttonText}>{action.title}</Text>
45
+ </Pressable>
46
+ ) : null}
47
+ </View>
48
+ );
49
+ }
50
+
51
+ export default MessageScreen;
@@ -0,0 +1,33 @@
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, { useMemo } from 'react';
18
+ import { DarkTheme, DefaultTheme, ThemeProvider } from 'expo-router';
19
+ import { useTheme } from '@lowdefy/client-native';
20
+
21
+ import getNavigationTheme from './getNavigationTheme.js';
22
+
23
+ // Gives the shell's navigators (stacks and tabs) the app's theme for the mode showing.
24
+ function NavigationTheme({ children }) {
25
+ const { isDark, tokens } = useTheme();
26
+ const value = useMemo(
27
+ () => getNavigationTheme({ base: isDark ? DarkTheme : DefaultTheme, isDark, tokens }),
28
+ [isDark, tokens]
29
+ );
30
+ return <ThemeProvider value={value}>{children}</ThemeProvider>;
31
+ }
32
+
33
+ export default NavigationTheme;
@@ -0,0 +1,51 @@
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, { useSyncExternalStore } from 'react';
18
+ import { StyleSheet, Text, View } from 'react-native';
19
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
20
+
21
+ import networkStatus from '../host/networkStatus.js';
22
+
23
+ const styles = StyleSheet.create({
24
+ banner: {
25
+ backgroundColor: '#434343',
26
+ paddingBottom: 6,
27
+ alignItems: 'center',
28
+ },
29
+ text: {
30
+ color: '#ffffff',
31
+ fontSize: 13,
32
+ },
33
+ });
34
+
35
+ // Shown in the frame while the shell is offline (networkStatus: the phone's network, or Lowdefy
36
+ // Dev's network injection). Cached pages still open; requests fail with the core's network
37
+ // error, as in a browser that lost its connection.
38
+ function NoConnectionBanner() {
39
+ const online = useSyncExternalStore(networkStatus.subscribe, networkStatus.isOnline);
40
+ const insets = useSafeAreaInsets();
41
+ if (online) {
42
+ return null;
43
+ }
44
+ return (
45
+ <View style={[styles.banner, { paddingTop: insets.top + 6 }]} testID="NoConnection">
46
+ <Text style={styles.text}>No connection</Text>
47
+ </View>
48
+ );
49
+ }
50
+
51
+ export default NoConnectionBanner;
@@ -0,0 +1,73 @@
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 { act, render, screen } from '@testing-library/react-native';
19
+
20
+ import createDeviceInjections from '../dev/inject/createDeviceInjections.js';
21
+ import NoConnectionBanner from './NoConnectionBanner.js';
22
+ import { OFFLINE, ONLINE } from '../test/createFakeNetwork.js';
23
+
24
+ // The phone's network, set by the tests through expo-network's listener.
25
+ const mockNetwork = {
26
+ listeners: new Set(),
27
+ state: { isConnected: true, isInternetReachable: true },
28
+ };
29
+ jest.mock('expo-network', () => ({
30
+ addNetworkStateListener(listener) {
31
+ mockNetwork.listeners.add(listener);
32
+ return { remove: () => mockNetwork.listeners.delete(listener) };
33
+ },
34
+ getNetworkStateAsync: async () => mockNetwork.state,
35
+ }));
36
+ jest.mock('react-native-safe-area-context', () => ({
37
+ useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
38
+ }));
39
+
40
+ // The shell's own online state, which the banner reads.
41
+ const networkStatus = jest.requireActual('../host/networkStatus.js').default;
42
+
43
+ function changeNetwork(state) {
44
+ mockNetwork.state = state;
45
+ mockNetwork.listeners.forEach((listener) => listener(state));
46
+ }
47
+
48
+ afterEach(() => {
49
+ networkStatus.setInjectedOffline(false);
50
+ changeNetwork(ONLINE);
51
+ });
52
+
53
+ test('the banner shows while the phone is offline', async () => {
54
+ await render(<NoConnectionBanner />);
55
+ await act(async () => {});
56
+ expect(screen.queryByTestId('NoConnection')).toBeNull();
57
+ await act(async () => changeNetwork(OFFLINE));
58
+ expect(screen.getByText('No connection')).toBeTruthy();
59
+ await act(async () => changeNetwork(ONLINE));
60
+ expect(screen.queryByTestId('NoConnection')).toBeNull();
61
+ });
62
+
63
+ // A simulator shares the computer's network: Lowdefy Dev's network injection is how an agent
64
+ // sees the banner there.
65
+ test("the banner shows while Lowdefy Dev's injection holds the network offline", async () => {
66
+ const injections = createDeviceInjections({ networkStatus });
67
+ await render(<NoConnectionBanner />);
68
+ await act(async () => {});
69
+ await act(async () => injections.inject({ kind: 'network', value: 'offline' }));
70
+ expect(screen.getByTestId('NoConnection')).toBeTruthy();
71
+ await act(async () => injections.inject({ kind: 'network', value: 'online' }));
72
+ expect(screen.queryByTestId('NoConnection')).toBeNull();
73
+ });
@@ -0,0 +1,56 @@
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, useState } from 'react';
18
+ import { Linking } from 'react-native';
19
+ import { router } from 'expo-router';
20
+
21
+ import LoadingScreen from './LoadingScreen.js';
22
+ import MessageScreen from './MessageScreen.js';
23
+ import { useShell } from '../ShellContext.js';
24
+
25
+ // A path no mobile page has. When the server has a web page there (it answers the shell with
26
+ // { surface: 'web' }), the page opens in the system browser; otherwise it is not found.
27
+ function OtherPageScreen({ path }) {
28
+ const { session } = useShell();
29
+ const [notFound, setNotFound] = useState(false);
30
+ useEffect(() => {
31
+ let active = true;
32
+ session
33
+ .fetchOtherPage(path)
34
+ .then((result) => {
35
+ if (!active) return;
36
+ if (result.surface === 'web') {
37
+ Linking.openURL(`${session.apiBase}/${path}`);
38
+ if (router.canGoBack()) router.back();
39
+ return;
40
+ }
41
+ setNotFound(true);
42
+ })
43
+ .catch(() => {
44
+ if (active) setNotFound(true);
45
+ });
46
+ return () => {
47
+ active = false;
48
+ };
49
+ }, [path, session]);
50
+ if (!notFound) {
51
+ return <LoadingScreen />;
52
+ }
53
+ return <MessageScreen testID="shell-not-found" title="Page not found" />;
54
+ }
55
+
56
+ export default OtherPageScreen;