@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,239 @@
|
|
|
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, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
18
|
+
import { Linking, LogBox, View } from 'react-native';
|
|
19
|
+
import * as SplashScreen from 'expo-splash-screen';
|
|
20
|
+
import { Accelerometer } from 'expo-sensors';
|
|
21
|
+
import { installDevice } from '@lowdefy/blocks-native/device';
|
|
22
|
+
import { installDictationSource } from '@lowdefy/blocks-native/dictation';
|
|
23
|
+
|
|
24
|
+
import AppProvider from '../components/AppProvider.js';
|
|
25
|
+
import ConfirmSheet from './connect/ConfirmSheet.js';
|
|
26
|
+
import ConnectScreen from './connect/ConnectScreen.js';
|
|
27
|
+
import createDevShellConfig from './connection/createDevShellConfig.js';
|
|
28
|
+
import createDeviceInjections from './inject/createDeviceInjections.js';
|
|
29
|
+
import createDeviceStore from '../host/createDeviceStore.js';
|
|
30
|
+
import createLogBuffer from './menu/createLogBuffer.js';
|
|
31
|
+
import createOfflineFetch from './inject/createOfflineFetch.js';
|
|
32
|
+
import createServerList, { SERVERS_KEY } from './connection/createServerList.js';
|
|
33
|
+
import DevMenu from './menu/DevMenu.js';
|
|
34
|
+
import DevTabClient from './tab/DevTabClient.js';
|
|
35
|
+
import Frame from '../components/Frame.js';
|
|
36
|
+
import getShellConfig from '../config/getShellConfig.js';
|
|
37
|
+
import device from '../device/device.js';
|
|
38
|
+
import injected, { installInjector } from '../device/injected.js';
|
|
39
|
+
import withInjectedDevice from './inject/withInjectedDevice.js';
|
|
40
|
+
import isLoopbackHost from './connection/isLoopbackHost.js';
|
|
41
|
+
import networkStatus from '../host/networkStatus.js';
|
|
42
|
+
import parseDevLink from './connection/parseDevLink.js';
|
|
43
|
+
import reloadApp from './tab/reloadApp.js';
|
|
44
|
+
import watchShake from './menu/watchShake.js';
|
|
45
|
+
|
|
46
|
+
// Lowdefy Dev reports errors in its own log (the dev menu) and to the dev server's build status.
|
|
47
|
+
// React Native's LogBox toasts would cover the tab bar and every screenshot an agent takes.
|
|
48
|
+
LogBox.ignoreAllLogs();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lowdefy Dev: the shell with developer features, in place of ShellRoot. It opens one app, the
|
|
52
|
+
* dev server it is connected to:
|
|
53
|
+
* - It connects to the last server it used, to a server a `lowdefy-dev://connect` link names, or
|
|
54
|
+
* to one picked on the connect screen (a scanned code, a typed address, a recent server). It asks
|
|
55
|
+
* "Connect to <host>?" first for any server not on this device: a simulator's or USB phone's
|
|
56
|
+
* localhost connects at once, which is how `lowdefy dev --ios` and `--android` open it.
|
|
57
|
+
* - Shaking the phone (or a `lowdefy-dev://menu` link) opens the dev menu.
|
|
58
|
+
* - The open app joins the dev server's tab channel (DevTabClient), which records what is done in
|
|
59
|
+
* it and takes device injections: the next scan, photo or NFC read, what the next dictation
|
|
60
|
+
* session hears, a location, a permission answer, or no network, which fails the app's every
|
|
61
|
+
* fetch and shows the NoConnection banner (createDeviceInjections).
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} props
|
|
64
|
+
* @param {Object} [props.accelerometer] expo-sensors' Accelerometer.
|
|
65
|
+
* @param {Object} [props.kv] The device store.
|
|
66
|
+
* @param {Object} [props.linking] React Native's Linking.
|
|
67
|
+
* @param {Object} [props.shell] The shell config.
|
|
68
|
+
*/
|
|
69
|
+
function DevShell({
|
|
70
|
+
accelerometer = Accelerometer,
|
|
71
|
+
kv: kvProp,
|
|
72
|
+
linking = Linking,
|
|
73
|
+
shell: shellProp,
|
|
74
|
+
}) {
|
|
75
|
+
const shell = useMemo(() => shellProp ?? getShellConfig(), [shellProp]);
|
|
76
|
+
const kv = useMemo(() => kvProp ?? createDeviceStore(), [kvProp]);
|
|
77
|
+
const servers = useMemo(() => createServerList({ kv }), [kv]);
|
|
78
|
+
const logs = useMemo(() => createLogBuffer({ console: globalThis.console }), []);
|
|
79
|
+
const injections = useMemo(() => createDeviceInjections({ networkStatus }), []);
|
|
80
|
+
const rootRef = useRef(null);
|
|
81
|
+
const openApp = useRef(null);
|
|
82
|
+
const [server, setServer] = useState(() => servers.last());
|
|
83
|
+
const [offered, setOffered] = useState(null);
|
|
84
|
+
const [message, setMessage] = useState(null);
|
|
85
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
86
|
+
const [openings, setOpenings] = useState(0);
|
|
87
|
+
|
|
88
|
+
const connect = useCallback(
|
|
89
|
+
(target) => {
|
|
90
|
+
servers.remember(target);
|
|
91
|
+
setOffered(null);
|
|
92
|
+
setMessage(null);
|
|
93
|
+
setServer({ url: target.url, host: target.host, pair: target.pair ?? null });
|
|
94
|
+
},
|
|
95
|
+
[servers]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const offer = useCallback(
|
|
99
|
+
(target) => {
|
|
100
|
+
if (isLoopbackHost(target.host)) {
|
|
101
|
+
connect(target);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
setOffered(target);
|
|
105
|
+
},
|
|
106
|
+
[connect]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const handleLink = useCallback(
|
|
110
|
+
(link) => {
|
|
111
|
+
const parsed = parseDevLink({ link, scheme: shell.scheme });
|
|
112
|
+
if (parsed === null) return false;
|
|
113
|
+
if (parsed.kind === 'menu') setMenuOpen(true);
|
|
114
|
+
if (parsed.kind === 'invalid') setMessage(parsed.reason);
|
|
115
|
+
if (parsed.kind === 'connect') offer(parsed);
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
[offer, shell.scheme]
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
useEffect(() => logs.install(), [logs]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => installInjector(injections), [injections]);
|
|
124
|
+
|
|
125
|
+
// The native inputs' dictation plays injected transcripts, and uses the microphone otherwise.
|
|
126
|
+
useEffect(() => installDictationSource(injected), []);
|
|
127
|
+
|
|
128
|
+
// ScanCode, QRScanner, PickImage, TakePhoto and ReadNfc answer injected results first.
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
installDevice(withInjectedDevice({ features: device.features, read: injected }));
|
|
131
|
+
return () => {
|
|
132
|
+
installDevice(device.features);
|
|
133
|
+
};
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
linking.getInitialURL().then((link) => link && handleLink(link));
|
|
138
|
+
const subscription = linking.addEventListener('url', ({ url }) => handleLink(url));
|
|
139
|
+
return () => subscription.remove();
|
|
140
|
+
}, [handleLink, linking]);
|
|
141
|
+
|
|
142
|
+
useEffect(() => watchShake({ accelerometer, onShake: () => setMenuOpen(true) }), [accelerometer]);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (server === null) SplashScreen.hideAsync();
|
|
146
|
+
}, [server]);
|
|
147
|
+
|
|
148
|
+
const devShell = useMemo(
|
|
149
|
+
() =>
|
|
150
|
+
server === null
|
|
151
|
+
? null
|
|
152
|
+
: createDevShellConfig({
|
|
153
|
+
fetch: createOfflineFetch({
|
|
154
|
+
fetch: globalThis.fetch,
|
|
155
|
+
isOffline: injections.isOffline,
|
|
156
|
+
}),
|
|
157
|
+
server,
|
|
158
|
+
shell,
|
|
159
|
+
}),
|
|
160
|
+
[injections, server, shell]
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const onApp = useCallback((app) => {
|
|
164
|
+
openApp.current = app;
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
// An app that could not open is opened again.
|
|
168
|
+
function reload() {
|
|
169
|
+
if (openApp.current === null) {
|
|
170
|
+
setOpenings((count) => count + 1);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
reloadApp({ session: openApp.current.session }).catch((error) => logs.add('error', error));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Signs out, which removes the session token and every cached page of this server's app, then
|
|
177
|
+
// removes the rest of what the device store holds for apps (keeping Lowdefy Dev's server list)
|
|
178
|
+
// and opens the app afresh.
|
|
179
|
+
async function clearData() {
|
|
180
|
+
if (openApp.current !== null) {
|
|
181
|
+
await openApp.current.auth.signOut();
|
|
182
|
+
}
|
|
183
|
+
kv.keys()
|
|
184
|
+
.filter((key) => key !== SERVERS_KEY)
|
|
185
|
+
.forEach((key) => kv.remove(key));
|
|
186
|
+
setOpenings((count) => count + 1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const sheet = (
|
|
190
|
+
<ConfirmSheet
|
|
191
|
+
onCancel={() => setOffered(null)}
|
|
192
|
+
onConfirm={() => connect(offered)}
|
|
193
|
+
server={offered}
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
const menu = (
|
|
197
|
+
<DevMenu
|
|
198
|
+
logs={logs}
|
|
199
|
+
onClearData={clearData}
|
|
200
|
+
onClose={() => setMenuOpen(false)}
|
|
201
|
+
onReload={reload}
|
|
202
|
+
onSwitchServer={() => setServer(null)}
|
|
203
|
+
server={server?.host ?? null}
|
|
204
|
+
shell={shell}
|
|
205
|
+
visible={menuOpen}
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (devShell === null) {
|
|
210
|
+
return (
|
|
211
|
+
<>
|
|
212
|
+
<ConnectScreen
|
|
213
|
+
message={message}
|
|
214
|
+
onOffer={offer}
|
|
215
|
+
onPick={connect}
|
|
216
|
+
onScanned={(text) => {
|
|
217
|
+
if (!handleLink(text)) setMessage('That code is not one lowdefy dev printed.');
|
|
218
|
+
}}
|
|
219
|
+
recent={servers.list()}
|
|
220
|
+
scheme={shell.scheme}
|
|
221
|
+
/>
|
|
222
|
+
{sheet}
|
|
223
|
+
{menu}
|
|
224
|
+
</>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return (
|
|
228
|
+
<View collapsable={false} ref={rootRef} style={{ flex: 1 }}>
|
|
229
|
+
<AppProvider key={`${devShell.app.id}:${openings}`} shell={devShell}>
|
|
230
|
+
<Frame />
|
|
231
|
+
<DevTabClient injections={injections} kv={kv} logs={logs} onApp={onApp} rootRef={rootRef} />
|
|
232
|
+
</AppProvider>
|
|
233
|
+
{sheet}
|
|
234
|
+
{menu}
|
|
235
|
+
</View>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export default DevShell;
|
|
@@ -0,0 +1,233 @@
|
|
|
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 { act, fireEvent, render, screen } from '@testing-library/react-native';
|
|
20
|
+
|
|
21
|
+
import createMemoryStore from '../host/createMemoryStore.js';
|
|
22
|
+
import DevShell from './DevShell.js';
|
|
23
|
+
|
|
24
|
+
// The opened app is recorded, not built: its auth, session and screens have their own tests.
|
|
25
|
+
const mockOpened = [];
|
|
26
|
+
jest.mock('../components/AppProvider.js', () => {
|
|
27
|
+
function AppProvider({ children, shell }) {
|
|
28
|
+
const { app } = shell;
|
|
29
|
+
mockOpened.push({ app, shell });
|
|
30
|
+
const { Text: MockText } = jest.requireActual('react-native');
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<MockText testID="opened-app">{app.urls.dev}</MockText>
|
|
34
|
+
{children}
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return AppProvider;
|
|
39
|
+
});
|
|
40
|
+
jest.mock('../components/Frame.js', () => () => null);
|
|
41
|
+
jest.mock('./tab/DevTabClient.js', () => () => null);
|
|
42
|
+
jest.mock('../host/createDeviceStore.js', () => () => {
|
|
43
|
+
throw new Error('The tests pass a device store.');
|
|
44
|
+
});
|
|
45
|
+
jest.mock('../config/getShellConfig.js', () => () => {
|
|
46
|
+
throw new Error('The tests pass the shell config.');
|
|
47
|
+
});
|
|
48
|
+
jest.mock('expo-splash-screen', () => ({ hideAsync: jest.fn() }));
|
|
49
|
+
jest.mock('expo-network', () => ({
|
|
50
|
+
addNetworkStateListener: () => ({ remove: () => undefined }),
|
|
51
|
+
getNetworkStateAsync: async () => ({ isConnected: true, isInternetReachable: true }),
|
|
52
|
+
}));
|
|
53
|
+
jest.mock('expo-sensors', () => ({ Accelerometer: {} }));
|
|
54
|
+
jest.mock('expo-camera', () => ({
|
|
55
|
+
CameraView: () => null,
|
|
56
|
+
useCameraPermissions: () => [{ granted: true }, jest.fn()],
|
|
57
|
+
}));
|
|
58
|
+
jest.mock('react-native-safe-area-context', () => ({
|
|
59
|
+
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
const shell = {
|
|
63
|
+
id: 'lowdefy-dev',
|
|
64
|
+
name: 'Lowdefy Dev',
|
|
65
|
+
environment: 'dev',
|
|
66
|
+
scheme: 'lowdefy-dev',
|
|
67
|
+
version: '7.0.0',
|
|
68
|
+
app: { id: 'dev', name: 'Lowdefy Dev', urls: { dev: 'http://localhost:3000' } },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function createLinking(initialUrl = null) {
|
|
72
|
+
const listeners = new Set();
|
|
73
|
+
return {
|
|
74
|
+
getInitialURL: jest.fn(async () => initialUrl),
|
|
75
|
+
addEventListener: (event, listener) => {
|
|
76
|
+
listeners.add(listener);
|
|
77
|
+
return { remove: () => listeners.delete(listener) };
|
|
78
|
+
},
|
|
79
|
+
open: (url) => listeners.forEach((listener) => listener({ url })),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createAccelerometer() {
|
|
84
|
+
let listener = null;
|
|
85
|
+
return {
|
|
86
|
+
setUpdateInterval: jest.fn(),
|
|
87
|
+
addListener: (fn) => {
|
|
88
|
+
listener = fn;
|
|
89
|
+
return { remove: () => undefined };
|
|
90
|
+
},
|
|
91
|
+
shake: () => [0, 1, 2].forEach(() => listener({ x: 2, y: 2, z: 2 })),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function renderDevShell({ initialUrl = null, kv = createMemoryStore() } = {}) {
|
|
96
|
+
const linking = createLinking(initialUrl);
|
|
97
|
+
const accelerometer = createAccelerometer();
|
|
98
|
+
await render(<DevShell accelerometer={accelerometer} kv={kv} linking={linking} shell={shell} />);
|
|
99
|
+
await act(async () => {});
|
|
100
|
+
return { accelerometer, kv, linking };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const lanLink = 'lowdefy-dev://connect?url=http%3A%2F%2F192.168.1.20%3A3000&pair=k3x9q';
|
|
104
|
+
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
mockOpened.splice(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('with no server, Lowdefy Dev shows the connect screen', async () => {
|
|
110
|
+
await renderDevShell();
|
|
111
|
+
expect(screen.getByTestId('dev-connect')).toBeTruthy();
|
|
112
|
+
expect(screen.queryByTestId('opened-app')).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('a connect link asks "Connect to <host>?" and attaches only once confirmed', async () => {
|
|
116
|
+
const { kv } = await renderDevShell({ initialUrl: lanLink });
|
|
117
|
+
expect(screen.getByText('Connect to 192.168.1.20:3000?')).toBeTruthy();
|
|
118
|
+
expect(screen.queryByTestId('opened-app')).toBeNull();
|
|
119
|
+
await act(async () => {
|
|
120
|
+
fireEvent.press(screen.getByTestId('dev-confirm-connect'));
|
|
121
|
+
});
|
|
122
|
+
expect(screen.getByTestId('opened-app').props.children).toBe('http://192.168.1.20:3000');
|
|
123
|
+
expect(JSON.parse(kv.get('lowdefy-dev:servers'))).toEqual([
|
|
124
|
+
{ url: 'http://192.168.1.20:3000', host: '192.168.1.20:3000', pair: 'k3x9q' },
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('the opened app sends the pairing token on every call', async () => {
|
|
129
|
+
await renderDevShell({ initialUrl: lanLink });
|
|
130
|
+
await act(async () => {
|
|
131
|
+
fireEvent.press(screen.getByTestId('dev-confirm-connect'));
|
|
132
|
+
});
|
|
133
|
+
const { app, shell: devShell } = mockOpened.at(-1);
|
|
134
|
+
expect(app.id).toBe('dev-192.168.1.20_3000');
|
|
135
|
+
const fetch = jest.fn(async () => ({ status: 200 }));
|
|
136
|
+
const pairedFetch = devShell.transport.wrapFetch(fetch);
|
|
137
|
+
await pairedFetch('http://192.168.1.20:3000/api/mobile/root');
|
|
138
|
+
expect(fetch.mock.calls[0][1].headers).toEqual({ 'x-lowdefy-pair': 'k3x9q' });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('a cancelled link attaches to nothing', async () => {
|
|
142
|
+
const { kv } = await renderDevShell({ initialUrl: lanLink });
|
|
143
|
+
await act(async () => {
|
|
144
|
+
fireEvent.press(screen.getByTestId('dev-confirm-cancel'));
|
|
145
|
+
});
|
|
146
|
+
expect(screen.queryByText('Connect to 192.168.1.20:3000?')).toBeNull();
|
|
147
|
+
expect(screen.queryByTestId('opened-app')).toBeNull();
|
|
148
|
+
expect(kv.get('lowdefy-dev:servers')).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('a typed address is confirmed before it is attached to', async () => {
|
|
152
|
+
await renderDevShell();
|
|
153
|
+
await act(async () => {
|
|
154
|
+
fireEvent.changeText(screen.getByTestId('dev-connect-url'), '192.168.1.20:3000');
|
|
155
|
+
});
|
|
156
|
+
await act(async () => {
|
|
157
|
+
fireEvent.press(screen.getByTestId('dev-connect-submit'));
|
|
158
|
+
});
|
|
159
|
+
expect(screen.getByText('Connect to 192.168.1.20:3000?')).toBeTruthy();
|
|
160
|
+
expect(screen.queryByTestId('opened-app')).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('a simulator link to localhost connects at once', async () => {
|
|
164
|
+
await renderDevShell({
|
|
165
|
+
initialUrl: 'lowdefy-dev://connect?url=http%3A%2F%2Flocalhost%3A3000',
|
|
166
|
+
});
|
|
167
|
+
expect(screen.queryByTestId('dev-confirm-sheet')).toBeNull();
|
|
168
|
+
expect(screen.getByTestId('opened-app').props.children).toBe('http://localhost:3000');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('the last server opens at launch, and a link to another one is confirmed first', async () => {
|
|
172
|
+
const kv = createMemoryStore();
|
|
173
|
+
kv.set(
|
|
174
|
+
'lowdefy-dev:servers',
|
|
175
|
+
JSON.stringify([{ url: 'http://10.0.0.5:3000', host: '10.0.0.5:3000', pair: 'old' }])
|
|
176
|
+
);
|
|
177
|
+
const { linking } = await renderDevShell({ kv });
|
|
178
|
+
expect(screen.getByTestId('opened-app').props.children).toBe('http://10.0.0.5:3000');
|
|
179
|
+
await act(async () => {
|
|
180
|
+
linking.open(lanLink);
|
|
181
|
+
});
|
|
182
|
+
expect(screen.getByText('Connect to 192.168.1.20:3000?')).toBeTruthy();
|
|
183
|
+
expect(screen.getByTestId('opened-app').props.children).toBe('http://10.0.0.5:3000');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('shaking the phone opens the dev menu, with the shell version', async () => {
|
|
187
|
+
const { accelerometer } = await renderDevShell({
|
|
188
|
+
initialUrl: 'lowdefy-dev://connect?url=http%3A%2F%2Flocalhost%3A3000',
|
|
189
|
+
});
|
|
190
|
+
expect(screen.queryByTestId('dev-menu')).toBeNull();
|
|
191
|
+
await act(async () => {
|
|
192
|
+
accelerometer.shake();
|
|
193
|
+
});
|
|
194
|
+
expect(screen.getByTestId('dev-menu')).toBeTruthy();
|
|
195
|
+
expect(screen.getByTestId('dev-menu-version').props.children).toBe(
|
|
196
|
+
'Shell 7.0.0 · localhost:3000'
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('a menu link opens the dev menu, and switch server goes back to the connect screen', async () => {
|
|
201
|
+
const { linking } = await renderDevShell({
|
|
202
|
+
initialUrl: 'lowdefy-dev://connect?url=http%3A%2F%2Flocalhost%3A3000',
|
|
203
|
+
});
|
|
204
|
+
await act(async () => {
|
|
205
|
+
linking.open('lowdefy-dev://menu');
|
|
206
|
+
});
|
|
207
|
+
await act(async () => {
|
|
208
|
+
fireEvent.press(screen.getByTestId('dev-menu-switch'));
|
|
209
|
+
});
|
|
210
|
+
expect(screen.getByTestId('dev-connect')).toBeTruthy();
|
|
211
|
+
expect(screen.getByTestId('dev-recent-localhost:3000')).toBeTruthy();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('the dev menu shows the shell logs and the block gallery entry', async () => {
|
|
215
|
+
jest.spyOn(console, 'info').mockImplementation(() => undefined);
|
|
216
|
+
const { linking } = await renderDevShell();
|
|
217
|
+
await act(async () => {
|
|
218
|
+
linking.open('lowdefy-dev://menu');
|
|
219
|
+
});
|
|
220
|
+
await act(async () => {
|
|
221
|
+
console.info('Dev tab: joined the dev server as tab phone.');
|
|
222
|
+
fireEvent.press(screen.getByTestId('dev-menu-logs'));
|
|
223
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
224
|
+
});
|
|
225
|
+
expect(screen.getByText(/Dev tab: joined the dev server as tab phone\./)).toBeTruthy();
|
|
226
|
+
await act(async () => {
|
|
227
|
+
fireEvent.press(screen.getByTestId('dev-menu-close'));
|
|
228
|
+
});
|
|
229
|
+
await act(async () => {
|
|
230
|
+
fireEvent.press(screen.getByTestId('dev-menu-gallery'));
|
|
231
|
+
});
|
|
232
|
+
expect(screen.getByTestId('dev-block-gallery')).toBeTruthy();
|
|
233
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
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 { Modal, Pressable, Text, View } from 'react-native';
|
|
19
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
20
|
+
|
|
21
|
+
import devStyles from '../devStyles.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* "Connect to <host>?", asked before Lowdefy Dev attaches to a server from a link, a scanned code
|
|
25
|
+
* or a typed address: a link can come from anyone, and the server it names sends every page the
|
|
26
|
+
* app shows.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} props
|
|
29
|
+
* @param {{ url, host, pair }|null} props.server The server to ask about; null hides the sheet.
|
|
30
|
+
* @param {() => void} props.onConfirm
|
|
31
|
+
* @param {() => void} props.onCancel
|
|
32
|
+
*/
|
|
33
|
+
function ConfirmSheet({ onCancel, onConfirm, server }) {
|
|
34
|
+
const insets = useSafeAreaInsets();
|
|
35
|
+
return (
|
|
36
|
+
<Modal animationType="slide" onRequestClose={onCancel} transparent visible={server !== null}>
|
|
37
|
+
<View style={devStyles.backdrop}>
|
|
38
|
+
<View
|
|
39
|
+
style={[devStyles.sheet, { paddingBottom: insets.bottom + 24 }]}
|
|
40
|
+
testID="dev-confirm-sheet"
|
|
41
|
+
>
|
|
42
|
+
<Text style={devStyles.sheetTitle}>{`Connect to ${server?.host}?`}</Text>
|
|
43
|
+
<Text style={devStyles.hint}>{server?.url}</Text>
|
|
44
|
+
<Text style={devStyles.hint}>
|
|
45
|
+
{server?.pair
|
|
46
|
+
? 'Paired with lowdefy dev --lan. Connect only to a server you started.'
|
|
47
|
+
: 'Connect only to a server you started.'}
|
|
48
|
+
</Text>
|
|
49
|
+
<Pressable
|
|
50
|
+
accessibilityRole="button"
|
|
51
|
+
onPress={onConfirm}
|
|
52
|
+
style={devStyles.button}
|
|
53
|
+
testID="dev-confirm-connect"
|
|
54
|
+
>
|
|
55
|
+
<Text style={devStyles.buttonText}>Connect</Text>
|
|
56
|
+
</Pressable>
|
|
57
|
+
<Pressable
|
|
58
|
+
accessibilityRole="button"
|
|
59
|
+
onPress={onCancel}
|
|
60
|
+
style={devStyles.secondaryButton}
|
|
61
|
+
testID="dev-confirm-cancel"
|
|
62
|
+
>
|
|
63
|
+
<Text style={devStyles.secondaryButtonText}>Cancel</Text>
|
|
64
|
+
</Pressable>
|
|
65
|
+
</View>
|
|
66
|
+
</View>
|
|
67
|
+
</Modal>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default ConfirmSheet;
|
|
@@ -0,0 +1,117 @@
|
|
|
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 { Pressable, ScrollView, Text, TextInput } from 'react-native';
|
|
19
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
20
|
+
|
|
21
|
+
import devStyles from '../devStyles.js';
|
|
22
|
+
import parseServerInput from '../connection/parseServerInput.js';
|
|
23
|
+
import QrScanner from './QrScanner.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Lowdefy Dev with no server: scan the code `lowdefy dev --lan` prints, type or paste its address,
|
|
27
|
+
* or pick one of the last 5 servers.
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} props
|
|
30
|
+
* @param {Array<{ url, host, pair }>} props.recent The servers connected to before, last first.
|
|
31
|
+
* @param {(server) => void} props.onOffer A server to confirm, from a code or a typed address.
|
|
32
|
+
* @param {(server) => void} props.onPick A server connected to before.
|
|
33
|
+
* @param {(text: string) => void} props.onScanned The text of a scanned code.
|
|
34
|
+
* @param {string} props.scheme The shell's URL scheme.
|
|
35
|
+
* @param {string|null} props.message A note to show, such as why a link could not be used.
|
|
36
|
+
*/
|
|
37
|
+
function ConnectScreen({ message, onOffer, onPick, onScanned, recent, scheme }) {
|
|
38
|
+
const insets = useSafeAreaInsets();
|
|
39
|
+
const [input, setInput] = useState('');
|
|
40
|
+
const [error, setError] = useState(null);
|
|
41
|
+
const [scanning, setScanning] = useState(false);
|
|
42
|
+
|
|
43
|
+
function submit() {
|
|
44
|
+
const parsed = parseServerInput({ input, scheme });
|
|
45
|
+
if (parsed.error) {
|
|
46
|
+
setError(parsed.error);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
setError(null);
|
|
50
|
+
onOffer(parsed);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ScrollView
|
|
55
|
+
contentContainerStyle={[devStyles.screen, { paddingTop: insets.top + 24 }]}
|
|
56
|
+
keyboardShouldPersistTaps="handled"
|
|
57
|
+
testID="dev-connect"
|
|
58
|
+
>
|
|
59
|
+
<Text style={devStyles.heading}>Lowdefy Dev</Text>
|
|
60
|
+
<Text style={devStyles.hint}>
|
|
61
|
+
Run lowdefy dev --lan and scan the code it prints, or enter the address it shows.
|
|
62
|
+
</Text>
|
|
63
|
+
{message ? <Text style={devStyles.error}>{message}</Text> : null}
|
|
64
|
+
<Pressable
|
|
65
|
+
accessibilityRole="button"
|
|
66
|
+
onPress={() => setScanning(true)}
|
|
67
|
+
style={devStyles.button}
|
|
68
|
+
testID="dev-connect-scan"
|
|
69
|
+
>
|
|
70
|
+
<Text style={devStyles.buttonText}>Scan code</Text>
|
|
71
|
+
</Pressable>
|
|
72
|
+
<TextInput
|
|
73
|
+
autoCapitalize="none"
|
|
74
|
+
autoCorrect={false}
|
|
75
|
+
inputMode="url"
|
|
76
|
+
onChangeText={setInput}
|
|
77
|
+
onSubmitEditing={submit}
|
|
78
|
+
placeholder="http://192.168.1.20:3000"
|
|
79
|
+
style={devStyles.input}
|
|
80
|
+
testID="dev-connect-url"
|
|
81
|
+
value={input}
|
|
82
|
+
/>
|
|
83
|
+
{error ? <Text style={devStyles.error}>{error}</Text> : null}
|
|
84
|
+
<Pressable
|
|
85
|
+
accessibilityRole="button"
|
|
86
|
+
onPress={submit}
|
|
87
|
+
style={devStyles.secondaryButton}
|
|
88
|
+
testID="dev-connect-submit"
|
|
89
|
+
>
|
|
90
|
+
<Text style={devStyles.secondaryButtonText}>Connect</Text>
|
|
91
|
+
</Pressable>
|
|
92
|
+
{recent.length > 0 ? <Text style={devStyles.hint}>Recent servers</Text> : null}
|
|
93
|
+
{recent.map((server) => (
|
|
94
|
+
<Pressable
|
|
95
|
+
accessibilityRole="button"
|
|
96
|
+
key={server.url}
|
|
97
|
+
onPress={() => onPick(server)}
|
|
98
|
+
style={devStyles.row}
|
|
99
|
+
testID={`dev-recent-${server.host}`}
|
|
100
|
+
>
|
|
101
|
+
<Text style={devStyles.rowText}>{server.host}</Text>
|
|
102
|
+
<Text style={devStyles.hint}>{server.url}</Text>
|
|
103
|
+
</Pressable>
|
|
104
|
+
))}
|
|
105
|
+
<QrScanner
|
|
106
|
+
onClose={() => setScanning(false)}
|
|
107
|
+
onScanned={(text) => {
|
|
108
|
+
setScanning(false);
|
|
109
|
+
onScanned(text);
|
|
110
|
+
}}
|
|
111
|
+
visible={scanning}
|
|
112
|
+
/>
|
|
113
|
+
</ScrollView>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default ConnectScreen;
|