@lowdefy/shell 3.23.3 → 7.2.0
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 +183 -183
- package/README.md +364 -2
- 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 +39 -0
- package/app/_layout.js +36 -0
- package/app.config.ts +183 -0
- package/appLinks.js +65 -0
- package/applyShellPlugins.js +43 -0
- package/authCallbackIntentFilters.js +46 -0
- package/babel.config.js +25 -0
- package/checkShellConfig.js +172 -0
- package/checkShellPlugins.js +74 -0
- package/getPrivacyManifest.js +39 -0
- package/metro.config.js +73 -0
- package/native/LowdefyAppShortcuts.swift.template +84 -0
- package/native/withAppShortcuts.js +60 -0
- package/native-fingerprint.json +16 -0
- package/package.json +120 -40
- package/profiles.js +607 -0
- package/readShellConfig.js +36 -0
- package/resolveShellPlugins.js +109 -0
- package/runtimeVersion.js +112 -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 +126 -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 +61 -0
- package/src/actions/GeolocationCurrentPosition.test.js +79 -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 +268 -0
- package/src/auth/SignInScreen.test.js +187 -0
- package/src/auth/createAppAuth.js +325 -0
- package/src/auth/createAppAuth.test.js +424 -0
- package/src/auth/createAuthApi.js +115 -0
- package/src/auth/createAuthApi.test.js +138 -0
- package/src/auth/createCoreAuth.js +96 -0
- package/src/auth/createCoreAuth.test.js +188 -0
- package/src/auth/createDeviceKey.js +62 -0
- package/src/auth/createDeviceKey.test.js +54 -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 +80 -0
- package/src/auth/purgeAppData.test.js +104 -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 +62 -0
- package/src/auth/unregisterPushToken.test.js +67 -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 +118 -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 +121 -0
- package/src/components/MenuButton.test.js +74 -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 +95 -0
- package/src/components/OtherPageScreen.test.js +74 -0
- package/src/components/PageScreen.js +254 -0
- package/src/components/PageScreen.test.js +492 -0
- package/src/components/RouteErrorScreen.js +47 -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 +40 -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 +34 -0
- package/src/config/getShellConfig.js +37 -0
- package/src/config/getShellConfig.test.js +80 -0
- package/src/createShellApp.js +126 -0
- package/src/dev/DevShell.js +239 -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 +188 -0
- package/src/dev/inject/createDeviceInjections.test.js +224 -0
- package/src/dev/inject/createOfflineFetch.js +35 -0
- package/src/dev/inject/createOfflineFetch.test.js +68 -0
- package/src/dev/inject/injectPush.js +60 -0
- package/src/dev/inject/injectShare.js +55 -0
- package/src/dev/inject/withInjectedDevice.js +98 -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/createDevTabClient.js +272 -0
- package/src/dev/tab/createDevTabClient.test.js +317 -0
- package/src/dev/tab/describeTabError.js +35 -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/DeviceHosts.js +32 -0
- package/src/device/basic/createFilesFeature.js +41 -0
- package/src/device/basic/createHapticsFeature.js +46 -0
- package/src/device/basic/createKeepAwakeFeature.js +37 -0
- package/src/device/basic/createPhotosFeature.js +40 -0
- package/src/device/basic/createRequestQueueFeature.js +37 -0
- package/src/device/basic/createShareFeature.js +63 -0
- package/src/device/basic/createShareFeature.test.js +47 -0
- package/src/device/basic/createSpeechFeature.js +43 -0
- package/src/device/basic/createUploadFeature.js +41 -0
- package/src/device/basic/toImagePickerOptions.js +41 -0
- package/src/device/basic/toPickedFile.js +54 -0
- package/src/device/calendar/index.js +68 -0
- package/src/device/contacts/index.js +67 -0
- package/src/device/createDevice.js +65 -0
- package/src/device/device.js +27 -0
- package/src/device/field/MapView.js +93 -0
- package/src/device/field/ScanCodeSheet.js +86 -0
- package/src/device/field/ScannerView.js +52 -0
- package/src/device/field/createAudioFeature.js +70 -0
- package/src/device/field/createBrightnessFeature.js +40 -0
- package/src/device/field/createCameraFeature.js +56 -0
- package/src/device/field/createDocumentScannerFeature.js +51 -0
- package/src/device/field/createLocationFeature.js +39 -0
- package/src/device/field/createScanRequests.js +55 -0
- package/src/device/field/index.js +39 -0
- package/src/device/field/scanRequests.js +22 -0
- package/src/device/injected.js +60 -0
- package/src/device/injected.test.js +39 -0
- package/src/device/nfc/index.js +89 -0
- package/src/device/nfc/toNfcRecord.js +44 -0
- package/src/device/nfc/toNfcRecord.test.js +48 -0
- package/src/device/tracking/createBacklog.js +63 -0
- package/src/device/tracking/createBacklog.test.js +47 -0
- package/src/device/tracking/index.js +211 -0
- package/src/device/tracking/index.test.js +110 -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 +62 -0
- package/src/host/createShellFetch.test.js +165 -0
- package/src/host/createShellGlobals.js +62 -0
- package/src/host/createShellHost.js +83 -0
- package/src/host/createShellHost.test.js +36 -0
- package/src/host/getOrigin.js +31 -0
- package/src/host/isNetworkError.js +24 -0
- package/src/host/isOnline.js +22 -0
- package/src/host/networkStatus.js +24 -0
- package/src/host/readServerError.js +45 -0
- package/src/host/readServerError.test.js +85 -0
- package/src/host/reportShellError.js +47 -0
- package/src/host/reportShellError.test.js +79 -0
- package/src/host/withNetworkErrors.js +38 -0
- package/src/host/withNetworkErrors.test.js +61 -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/native/backgroundFetch.js +97 -0
- package/src/native/callEndpoint.js +63 -0
- package/src/native/createAppEventTrigger.js +70 -0
- package/src/native/currentApp.js +49 -0
- package/src/native/getPagePath.js +32 -0
- package/src/native/openPath.js +55 -0
- package/src/native/openPath.test.js +39 -0
- package/src/native/parseShellLink.js +38 -0
- package/src/native/parseShellLink.test.js +29 -0
- package/src/native/startAppServices.js +94 -0
- package/src/native/startAppServices.test.js +97 -0
- package/src/native/startShareIn.js +59 -0
- package/src/native/startShortcuts.js +88 -0
- package/src/native/startShortcuts.test.js +59 -0
- package/src/native/toSharePayload.js +47 -0
- package/src/native/toSharePayload.test.js +47 -0
- package/src/native/whenAppReady.js +48 -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/toAppLinkPath.js +43 -0
- package/src/navigation/toAppLinkPath.test.js +53 -0
- package/src/navigation/toRouterHref.js +29 -0
- package/src/notifications/createNotificationsFeature.js +61 -0
- package/src/notifications/getCategoryAction.js +31 -0
- package/src/notifications/handleNotificationResponse.js +82 -0
- package/src/notifications/registerPushToken.js +71 -0
- package/src/notifications/startNotifications.js +114 -0
- package/src/notifications/toCategoryActions.js +42 -0
- package/src/notifications/toNotificationEvent.js +36 -0
- package/src/notifications/toScheduleRequest.js +62 -0
- package/src/plugins/appTypes.js +28 -0
- package/src/plugins/prefixTypes.js +28 -0
- package/src/plugins/shellPlugins.json +15 -0
- package/src/plugins/typeManifest.js +789 -0
- package/src/plugins/types.js +64 -0
- package/src/plugins/types.test.js +173 -0
- package/src/polyfills/installGetRandomValues.js +28 -0
- package/src/queue/createRequestQueue.js +198 -0
- package/src/queue/createRequestQueue.test.js +215 -0
- package/src/queue/createSqliteQueueStore.js +121 -0
- package/src/queue/summarizeQueue.js +39 -0
- package/src/session/createAppSession.js +323 -0
- package/src/session/createAppSession.test.js +391 -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 +72 -0
- package/src/sync/fetchPage.test.js +84 -0
- package/src/sync/fetchRoot.js +43 -0
- package/src/sync/fetchUser.js +33 -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/createNetworkError.js +24 -0
- package/src/test/expoImage.js +42 -0
- package/src/test/expoNotifications.js +40 -0
- package/src/test/expoQuickActions.js +22 -0
- package/src/test/expoSpeechRecognition.js +31 -0
- package/src/test/expoTaskManager.js +25 -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/nativeModule.js +33 -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/src/widgets/fetchWidgetData.js +59 -0
- package/src/widgets/index.js +156 -0
- package/src/widgets/refreshWidgets.js +31 -0
- package/withExcludedNativeModules.js +93 -0
- package/dist/location.js +0 -33
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/icon-32.png +0 -0
- package/dist/public/icon-512.png +0 -0
- package/dist/public/icon.svg +0 -17
- package/dist/public/logo-dark-theme.png +0 -0
- package/dist/public/logo-light-theme.png +0 -0
- package/dist/public/logo-square-dark-theme.png +0 -0
- package/dist/public/logo-square-light-theme.png +0 -0
- package/dist/public/manifest.webmanifest +0 -16
- package/dist/shell/246.93432ff68186aae6c87b.js +0 -2
- package/dist/shell/246.93432ff68186aae6c87b.js.LICENSE.txt +0 -8
- package/dist/shell/454.b597ca21366afda000af.js +0 -2
- package/dist/shell/454.b597ca21366afda000af.js.LICENSE.txt +0 -14
- package/dist/shell/484.d645e6c09079f45ce044.js +0 -1
- package/dist/shell/706.a66931e02a549fd269bd.js +0 -2
- package/dist/shell/706.a66931e02a549fd269bd.js.LICENSE.txt +0 -8
- package/dist/shell/9.78be8c1b6c3986f404fc.js +0 -2
- package/dist/shell/9.78be8c1b6c3986f404fc.js.LICENSE.txt +0 -45
- package/dist/shell/922.43b7a3ebe22ef05b3049.js +0 -2
- package/dist/shell/922.43b7a3ebe22ef05b3049.js.LICENSE.txt +0 -14
- package/dist/shell/index.html +0 -57
- package/dist/shell/main.f01501a75909f38ccc32.js +0 -1
- package/dist/shell/runtime.e50f6c3a5f7c74a0a717.js +0 -1
|
@@ -0,0 +1,74 @@
|
|
|
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 { iosColors, ThemeContext } from '@lowdefy/client-native';
|
|
23
|
+
import ShellContext from '../ShellContext.js';
|
|
24
|
+
|
|
25
|
+
jest.mock('expo-router', () => ({
|
|
26
|
+
router: { canDismiss: () => false, replace: jest.fn() },
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
jest.mock('react-native-safe-area-context', () => ({
|
|
30
|
+
useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 0, left: 0 }),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const house = { node: [['path', { d: 'M3 10a2 2 0 0 1 .709-1.528l7-6' }]] };
|
|
34
|
+
|
|
35
|
+
test('the drawer menu draws each item icon from the icon data the root carries', async () => {
|
|
36
|
+
const state = { root: { pageIndex: [{ pageId: 'today', path: 'today' }] } };
|
|
37
|
+
const session = { getState: () => state, subscribe: () => () => undefined };
|
|
38
|
+
await render(
|
|
39
|
+
<ShellContext.Provider value={{ session }}>
|
|
40
|
+
<MenuButton
|
|
41
|
+
items={[
|
|
42
|
+
{ pageId: 'today', title: 'Today', icon: 'House', iconData: house },
|
|
43
|
+
{ pageId: 'jobs', title: 'Jobs' },
|
|
44
|
+
]}
|
|
45
|
+
/>
|
|
46
|
+
</ShellContext.Provider>
|
|
47
|
+
);
|
|
48
|
+
fireEvent.press(screen.getByTestId('shell-menu'));
|
|
49
|
+
expect(await screen.findByTestId('menu-today-icon')).toBeTruthy();
|
|
50
|
+
expect(screen.queryByTestId('menu-jobs-icon')).toBeNull();
|
|
51
|
+
expect(screen.getByText('Jobs')).toBeTruthy();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the drawer menu takes the theme tokens in dark mode', async () => {
|
|
55
|
+
const state = { root: { pageIndex: [] } };
|
|
56
|
+
const session = { getState: () => state, subscribe: () => () => undefined };
|
|
57
|
+
const tokens = { ...iosColors.dark, colorTint: '#0A84FF' };
|
|
58
|
+
await render(
|
|
59
|
+
<ThemeContext.Provider value={{ isDark: true, tokens }}>
|
|
60
|
+
<ShellContext.Provider value={{ session }}>
|
|
61
|
+
<MenuButton items={[{ pageId: 'jobs', title: 'Jobs', iconData: house }]} />
|
|
62
|
+
</ShellContext.Provider>
|
|
63
|
+
</ThemeContext.Provider>
|
|
64
|
+
);
|
|
65
|
+
expect(screen.getByText('Menu')).toHaveStyle({ color: '#0A84FF' });
|
|
66
|
+
fireEvent.press(screen.getByTestId('shell-menu'));
|
|
67
|
+
expect(await screen.findByText('Jobs')).toHaveStyle({ color: tokens.colorLabel });
|
|
68
|
+
expect(screen.getByTestId('menu-jobs-icon', { includeHiddenElements: true }).props.color).toBe(
|
|
69
|
+
tokens.colorLabel
|
|
70
|
+
);
|
|
71
|
+
expect(screen.getByText('Jobs').parent.parent.parent).toHaveStyle({
|
|
72
|
+
backgroundColor: tokens.colorGroupedBackground,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -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,95 @@
|
|
|
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 reportShellError from '../host/reportShellError.js';
|
|
24
|
+
import { useShell } from '../ShellContext.js';
|
|
25
|
+
|
|
26
|
+
// A path no mobile page has. When the server has a web page there (it answers the shell with
|
|
27
|
+
// { surface: 'web' }), the page opens in the system browser; otherwise it is not found. A request
|
|
28
|
+
// or browser that failed is not "not found": it says so, and the fault is reported.
|
|
29
|
+
function OtherPageScreen({ path, query = '' }) {
|
|
30
|
+
const { session } = useShell();
|
|
31
|
+
const [view, setView] = useState('loading');
|
|
32
|
+
const [loadError, setLoadError] = useState(null);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
let active = true;
|
|
35
|
+
function fail(error) {
|
|
36
|
+
if (!active) return;
|
|
37
|
+
reportShellError({ error, session });
|
|
38
|
+
setLoadError(error);
|
|
39
|
+
setView('failed');
|
|
40
|
+
}
|
|
41
|
+
session
|
|
42
|
+
.fetchOtherPage(path)
|
|
43
|
+
.then((result) => {
|
|
44
|
+
if (!active) return;
|
|
45
|
+
if (result.surface === 'web') {
|
|
46
|
+
// The query rides along: the web page reads its own _url_query.
|
|
47
|
+
const url = `${session.apiBase}/${path}${query ? `?${query}` : ''}`;
|
|
48
|
+
Linking.openURL(url)
|
|
49
|
+
.then(() => {
|
|
50
|
+
if (!active) return;
|
|
51
|
+
if (router.canGoBack()) {
|
|
52
|
+
router.back();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// A cold start from a deep link has nothing to go back to. Going home on its own
|
|
56
|
+
// would open the browser again when the home page is itself web-only.
|
|
57
|
+
setView('opened');
|
|
58
|
+
})
|
|
59
|
+
.catch(fail);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
setView('notFound');
|
|
63
|
+
})
|
|
64
|
+
.catch(fail);
|
|
65
|
+
return () => {
|
|
66
|
+
active = false;
|
|
67
|
+
};
|
|
68
|
+
}, [path, query, session]);
|
|
69
|
+
if (view === 'opened') {
|
|
70
|
+
return (
|
|
71
|
+
<MessageScreen
|
|
72
|
+
action={{ title: 'Go home', onPress: () => router.replace('/') }}
|
|
73
|
+
body="This page is on the web, not in the app."
|
|
74
|
+
testID="shell-opened-in-browser"
|
|
75
|
+
title="Opened in your browser"
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (view === 'failed') {
|
|
80
|
+
return (
|
|
81
|
+
<MessageScreen
|
|
82
|
+
action={{ title: 'Go home', onPress: () => router.replace('/') }}
|
|
83
|
+
body={loadError?.message}
|
|
84
|
+
testID="shell-other-page-error"
|
|
85
|
+
title="This page could not be opened"
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (view === 'notFound') {
|
|
90
|
+
return <MessageScreen testID="shell-not-found" title="Page not found" />;
|
|
91
|
+
}
|
|
92
|
+
return <LoadingScreen />;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default OtherPageScreen;
|
|
@@ -0,0 +1,74 @@
|
|
|
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 { Linking } from 'react-native';
|
|
20
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
|
|
21
|
+
import { router } from 'expo-router';
|
|
22
|
+
|
|
23
|
+
import OtherPageScreen from './OtherPageScreen.js';
|
|
24
|
+
import ShellContext from '../ShellContext.js';
|
|
25
|
+
|
|
26
|
+
jest.mock('expo-router', () => ({
|
|
27
|
+
router: { back: jest.fn(), canGoBack: jest.fn(), replace: jest.fn() },
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
function renderScreen(result = { surface: 'web' }) {
|
|
31
|
+
const session = {
|
|
32
|
+
apiBase: 'https://app.test',
|
|
33
|
+
fetchOtherPage: jest.fn().mockResolvedValue(result),
|
|
34
|
+
getState: () => ({ lowdefy: { buildId: 'b1' } }),
|
|
35
|
+
};
|
|
36
|
+
return render(
|
|
37
|
+
<ShellContext.Provider value={{ session }}>
|
|
38
|
+
<OtherPageScreen path="reports" query="id=7" />
|
|
39
|
+
</ShellContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
jest.clearAllMocks();
|
|
45
|
+
jest.spyOn(Linking, 'openURL').mockResolvedValue(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('a cold start opens the web page with its query and offers home, going nowhere on its own', async () => {
|
|
49
|
+
router.canGoBack.mockReturnValue(false);
|
|
50
|
+
await renderScreen();
|
|
51
|
+
expect(await screen.findByTestId('shell-opened-in-browser')).toBeTruthy();
|
|
52
|
+
expect(Linking.openURL).toHaveBeenCalledWith('https://app.test/reports?id=7');
|
|
53
|
+
expect(router.back).not.toHaveBeenCalled();
|
|
54
|
+
// Home could be this web-only page, which would open the browser again.
|
|
55
|
+
expect(router.replace).not.toHaveBeenCalled();
|
|
56
|
+
fireEvent.press(screen.getByText('Go home'));
|
|
57
|
+
expect(router.replace).toHaveBeenCalledWith('/');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('with history it goes back after opening the browser', async () => {
|
|
61
|
+
router.canGoBack.mockReturnValue(true);
|
|
62
|
+
await renderScreen();
|
|
63
|
+
await waitFor(() => expect(router.back).toHaveBeenCalled());
|
|
64
|
+
expect(router.replace).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('a failed openURL says the page could not be opened, not that it was not found', async () => {
|
|
68
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
69
|
+
Linking.openURL.mockRejectedValue(new Error('no browser'));
|
|
70
|
+
await renderScreen();
|
|
71
|
+
expect(await screen.findByTestId('shell-other-page-error')).toBeTruthy();
|
|
72
|
+
expect(screen.getByText('no browser')).toBeTruthy();
|
|
73
|
+
consoleError.mockRestore();
|
|
74
|
+
});
|