@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.
- package/LICENSE +201 -0
- package/README.md +308 -0
- package/app/(t0,t1,t2,t3,t4)/[...path].js +38 -0
- package/app/(t0,t1,t2,t3,t4)/_layout.js +48 -0
- package/app/(t0,t1,t2,t3,t4)/index.js +38 -0
- package/app/+native-intent.js +30 -0
- package/app/_layout.js +33 -0
- package/app.config.ts +168 -0
- package/applyShellPlugins.js +43 -0
- package/authCallbackIntentFilters.js +46 -0
- package/babel.config.js +25 -0
- package/checkShellConfig.js +145 -0
- package/checkShellPlugins.js +74 -0
- package/getPrivacyManifest.js +39 -0
- package/metro.config.js +73 -0
- package/native-fingerprint.json +12 -0
- package/package.json +130 -0
- package/profiles.js +274 -0
- package/readShellConfig.js +36 -0
- package/resolveShellPlugins.js +109 -0
- package/runtimeVersion.js +105 -0
- package/scripts/checkReleaseBundle.mjs +358 -0
- package/scripts/createDayjsLocales.mjs +67 -0
- package/scripts/createShellAssets.mjs +54 -0
- package/scripts/createShellVersion.mjs +83 -0
- package/scripts/createTypeManifest.mjs +205 -0
- package/shell.dev.json +11 -0
- package/shellVariant.js +111 -0
- package/src/ShellContext.js +32 -0
- package/src/actions/CopyToClipboard.js +29 -0
- package/src/actions/CopyToClipboard.test.js +33 -0
- package/src/actions/GeolocationCurrentPosition.js +55 -0
- package/src/actions/GeolocationCurrentPosition.test.js +66 -0
- package/src/actions/GetLocalStorage.js +36 -0
- package/src/actions/RemoveLocalStorage.js +31 -0
- package/src/actions/ScrollTo.js +66 -0
- package/src/actions/ScrollTo.test.js +83 -0
- package/src/actions/SetFocus.js +33 -0
- package/src/actions/SetFocus.test.js +49 -0
- package/src/actions/SetLocalStorage.js +34 -0
- package/src/actions/localStorage.test.js +68 -0
- package/src/actions/measureInScrollView.js +36 -0
- package/src/actions/nativeActions.js +41 -0
- package/src/auth/AuthButton.js +44 -0
- package/src/auth/AuthGate.js +57 -0
- package/src/auth/AuthLink.js +43 -0
- package/src/auth/LockScreen.js +67 -0
- package/src/auth/LockScreen.test.js +37 -0
- package/src/auth/SignInScreen.js +256 -0
- package/src/auth/SignInScreen.test.js +182 -0
- package/src/auth/createAppAuth.js +306 -0
- package/src/auth/createAppAuth.test.js +414 -0
- package/src/auth/createAuthApi.js +99 -0
- package/src/auth/createAuthApi.test.js +135 -0
- package/src/auth/createCoreAuth.js +96 -0
- package/src/auth/createCoreAuth.test.js +188 -0
- package/src/auth/createSessionStores.js +45 -0
- package/src/auth/createTokenStore.js +103 -0
- package/src/auth/createTokenStore.test.js +116 -0
- package/src/auth/getAuthSettings.js +24 -0
- package/src/auth/getAuthView.js +39 -0
- package/src/auth/getAuthView.test.js +52 -0
- package/src/auth/getSheetReturnUrl.js +35 -0
- package/src/auth/getTokenStoreOptions.js +46 -0
- package/src/auth/isBackgroundShell.js +26 -0
- package/src/auth/isSheetReturnLink.js +35 -0
- package/src/auth/parseQuery.js +39 -0
- package/src/auth/purgeAppData.js +45 -0
- package/src/auth/shouldLock.js +26 -0
- package/src/auth/signInWithSheet.js +97 -0
- package/src/auth/signInWithSheet.test.js +205 -0
- package/src/auth/styles.js +78 -0
- package/src/auth/unregisterPushToken.js +21 -0
- package/src/auth/usesBiometricUnlock.js +23 -0
- package/src/auth/watchSession.js +47 -0
- package/src/auth/withoutCookies.js +26 -0
- package/src/cache/createSqliteCacheStore.js +113 -0
- package/src/cache/createSqliteCacheStore.test.js +93 -0
- package/src/cache/getCacheKey.js +25 -0
- package/src/cache/getLastCacheKeyName.js +22 -0
- package/src/components/AppProvider.js +90 -0
- package/src/components/Frame.js +116 -0
- package/src/components/Frame.test.js +101 -0
- package/src/components/LoadingScreen.js +35 -0
- package/src/components/MenuButton.js +108 -0
- package/src/components/MenuButton.test.js +51 -0
- package/src/components/MessageScreen.js +51 -0
- package/src/components/NavigationTheme.js +33 -0
- package/src/components/NoConnectionBanner.js +51 -0
- package/src/components/NoConnectionBanner.test.js +73 -0
- package/src/components/OtherPageScreen.js +56 -0
- package/src/components/PageScreen.js +244 -0
- package/src/components/PageScreen.test.js +492 -0
- package/src/components/ShellRoot.js +34 -0
- package/src/components/ShellRoot.test.js +62 -0
- package/src/components/ShellThemeProvider.js +45 -0
- package/src/components/SwitchToTab.js +36 -0
- package/src/components/SwitchToTab.test.js +63 -0
- package/src/components/TabIconImages.js +72 -0
- package/src/components/UpdateScreen.js +36 -0
- package/src/components/getNavigationTheme.js +44 -0
- package/src/components/getNavigationTheme.test.js +41 -0
- package/src/components/styles.js +50 -0
- package/src/components/useSurfaceTokens.js +47 -0
- package/src/config/getAppApiBase.js +31 -0
- package/src/config/getShellConfig.js +35 -0
- package/src/config/getShellConfig.test.js +80 -0
- package/src/createShellApp.js +105 -0
- package/src/dev/DevShell.js +228 -0
- package/src/dev/DevShell.test.js +233 -0
- package/src/dev/connect/ConfirmSheet.js +71 -0
- package/src/dev/connect/ConnectScreen.js +117 -0
- package/src/dev/connect/QrScanner.js +89 -0
- package/src/dev/connection/createDevShellConfig.js +47 -0
- package/src/dev/connection/createDevTransport.js +51 -0
- package/src/dev/connection/createDevTransport.test.js +87 -0
- package/src/dev/connection/createServerList.js +48 -0
- package/src/dev/connection/isLoopbackHost.js +25 -0
- package/src/dev/connection/pairedShellApp.test.js +104 -0
- package/src/dev/connection/parseDevLink.js +66 -0
- package/src/dev/connection/parseDevLink.test.js +97 -0
- package/src/dev/connection/parseQuery.js +33 -0
- package/src/dev/connection/parseServerInput.js +40 -0
- package/src/dev/connection/parseServerUrl.js +45 -0
- package/src/dev/devStyles.js +97 -0
- package/src/dev/index.js +21 -0
- package/src/dev/inject/createDeviceInjections.js +185 -0
- package/src/dev/inject/createDeviceInjections.test.js +208 -0
- package/src/dev/inject/createOfflineFetch.js +35 -0
- package/src/dev/inject/createOfflineFetch.test.js +68 -0
- package/src/dev/menu/BlockGallery.js +33 -0
- package/src/dev/menu/DevMenu.js +121 -0
- package/src/dev/menu/createLogBuffer.js +83 -0
- package/src/dev/menu/watchShake.js +52 -0
- package/src/dev/menu/watchShake.test.js +61 -0
- package/src/dev/recorder/createNativeRecorder.js +204 -0
- package/src/dev/recorder/createNativeRecorder.test.js +307 -0
- package/src/dev/redirectDevLink.js +30 -0
- package/src/dev/redirectDevLink.test.js +49 -0
- package/src/dev/steps/createStepRunner.js +184 -0
- package/src/dev/steps/createStepRunner.test.js +262 -0
- package/src/dev/steps/findBlock.js +78 -0
- package/src/dev/steps/isContextSettled.js +37 -0
- package/src/dev/steps/waitForSettle.js +39 -0
- package/src/dev/steps/waitUntil.js +40 -0
- package/src/dev/tab/DevTabClient.js +97 -0
- package/src/dev/tab/answerEval.js +39 -0
- package/src/dev/tab/answerInspect.js +45 -0
- package/src/dev/tab/answerLoadState.js +50 -0
- package/src/dev/tab/createDevTabClient.js +277 -0
- package/src/dev/tab/createDevTabClient.test.js +314 -0
- package/src/dev/tab/devClientMarker.js +22 -0
- package/src/dev/tab/devClientMarker.test.js +23 -0
- package/src/dev/tab/findPageContext.js +43 -0
- package/src/dev/tab/getShownPage.js +42 -0
- package/src/dev/tab/readEventStream.js +68 -0
- package/src/dev/tab/reloadApp.js +42 -0
- package/src/dev/tab/reloadApp.test.js +115 -0
- package/src/device/injected.js +60 -0
- package/src/device/injected.test.js +39 -0
- package/src/host/createDeviceStore.js +32 -0
- package/src/host/createMemoryStore.js +32 -0
- package/src/host/createNetworkStatus.js +94 -0
- package/src/host/createNetworkStatus.test.js +94 -0
- package/src/host/createRouterAdapter.js +33 -0
- package/src/host/createShellFetch.js +60 -0
- package/src/host/createShellFetch.test.js +150 -0
- package/src/host/createShellGlobals.js +62 -0
- package/src/host/createShellHost.js +73 -0
- package/src/host/createShellHost.test.js +36 -0
- package/src/host/getOrigin.js +26 -0
- package/src/host/isOnline.js +22 -0
- package/src/host/networkStatus.js +24 -0
- package/src/host/withShellHeader.js +25 -0
- package/src/host/withShellHeader.test.js +28 -0
- package/src/js/evaluateJsEntries.js +43 -0
- package/src/js/evaluateJsEntries.test.js +48 -0
- package/src/locale/createShellLocale.js +112 -0
- package/src/locale/createShellLocale.test.js +74 -0
- package/src/locale/dayjsLocales.js +165 -0
- package/src/locale/getDeviceLocales.js +23 -0
- package/src/locale/loadDayjsLocale.js +32 -0
- package/src/locale/resolveActiveLocale.js +46 -0
- package/src/locale/resolveActiveLocale.test.js +40 -0
- package/src/navigation/SlotContext.js +24 -0
- package/src/navigation/frameSlots.js +27 -0
- package/src/navigation/getFrame.js +30 -0
- package/src/navigation/getInitialScreenOptions.js +63 -0
- package/src/navigation/getInitialScreenOptions.test.js +66 -0
- package/src/navigation/getPresentation.js +28 -0
- package/src/navigation/getSlotRootPath.js +27 -0
- package/src/navigation/getStackOptions.js +185 -0
- package/src/navigation/getTabHref.js +45 -0
- package/src/navigation/getTabIcons.js +43 -0
- package/src/navigation/isPageUnderHeader.js +46 -0
- package/src/navigation/navigation.test.js +342 -0
- package/src/navigation/toRouterHref.js +29 -0
- package/src/plugins/appTypes.js +28 -0
- package/src/plugins/prefixTypes.js +28 -0
- package/src/plugins/shellPlugins.json +14 -0
- package/src/plugins/typeManifest.js +649 -0
- package/src/plugins/types.js +61 -0
- package/src/plugins/types.test.js +173 -0
- package/src/polyfills/installGetRandomValues.js +28 -0
- package/src/session/createAppSession.js +318 -0
- package/src/session/createAppSession.test.js +359 -0
- package/src/session/getPageView.js +59 -0
- package/src/session/getPageView.test.js +137 -0
- package/src/sync/encodePagePath.js +27 -0
- package/src/sync/fetchPage.js +49 -0
- package/src/sync/fetchRoot.js +38 -0
- package/src/sync/fetchUser.js +28 -0
- package/src/sync/runLimited.js +36 -0
- package/src/sync/toRootConfig.js +35 -0
- package/src/test/createFakeNetwork.js +40 -0
- package/src/test/createFakeSecureStore.js +40 -0
- package/src/test/createFakeServer.js +98 -0
- package/src/test/createMemoryCacheStore.js +59 -0
- package/src/test/expoImage.js +38 -0
- package/src/test/expoSpeechRecognition.js +31 -0
- package/src/test/expoUi.js +55 -0
- package/src/test/expoUiBottomSheet.js +64 -0
- package/src/test/expoUiDateTimePicker.js +26 -0
- package/src/test/expoUiMenu.js +46 -0
- package/src/test/expoUiSegmentedControl.js +47 -0
- package/src/test/expoUiSwiftUi.js +62 -0
- package/src/test/expoUiSwiftUiModifiers.js +23 -0
- package/src/test/expoWebBrowser.js +23 -0
- package/src/test/fixtures.js +98 -0
- package/src/test/renderPageScreen.js +109 -0
- package/src/types/checkTypeSet.js +39 -0
- package/src/types/collectPageTypes.js +49 -0
- package/src/types/collectPageTypes.test.js +90 -0
- package/src/types/findMissingPageTypes.js +40 -0
- package/src/version/compareVersions.js +38 -0
- package/src/version/isShellTooOld.js +30 -0
- package/src/version/shellVersion.js +21 -0
- package/src/version/shellVersion.test.js +32 -0
- 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;
|