@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,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import parseDevLink from './parseDevLink.js';
|
|
18
|
+
import parseServerUrl from './parseServerUrl.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What a developer typed or pasted on the connect screen: a server URL (`192.168.1.20:3000`,
|
|
22
|
+
* `http://...`) or the whole link `lowdefy dev --lan` prints. Returns `{ url, host, pair }`, or
|
|
23
|
+
* `{ error }`.
|
|
24
|
+
*/
|
|
25
|
+
function parseServerInput({ input, scheme }) {
|
|
26
|
+
const link = parseDevLink({ link: input?.trim(), scheme });
|
|
27
|
+
if (link?.kind === 'connect') {
|
|
28
|
+
return { url: link.url, host: link.host, pair: link.pair };
|
|
29
|
+
}
|
|
30
|
+
if (link?.kind === 'invalid') {
|
|
31
|
+
return { error: link.reason };
|
|
32
|
+
}
|
|
33
|
+
const server = parseServerUrl(input);
|
|
34
|
+
if (server === null) {
|
|
35
|
+
return { error: 'Enter the address lowdefy dev prints, like http://192.168.1.20:3000.' };
|
|
36
|
+
}
|
|
37
|
+
return { url: server.url, host: server.host, pair: null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default parseServerInput;
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
// The host excludes "@" and a backslash: a URL with userinfo (http://localhost@evil.example)
|
|
20
|
+
// names a different host than its text starts with, and would pass for a loopback server.
|
|
21
|
+
const SERVER_URL = /^(https?):\/\/([^/\\?#\s@]+)(\/[^?#\s]*)?$/i;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A dev server's URL as Lowdefy Dev keeps it: `{ url, host }`, the url without a trailing slash,
|
|
25
|
+
* or null when it is not an http(s) URL. A bare `host:port` is read as http, the way lowdefy dev
|
|
26
|
+
* serves.
|
|
27
|
+
*/
|
|
28
|
+
function parseServerUrl(input) {
|
|
29
|
+
if (!type.isString(input)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const trimmed = input.trim();
|
|
33
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
34
|
+
const match = SERVER_URL.exec(withScheme);
|
|
35
|
+
if (match === null) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const [, scheme, host, pathname = ''] = match;
|
|
39
|
+
return {
|
|
40
|
+
url: `${scheme.toLowerCase()}://${host}${pathname.replace(/\/+$/, '')}`,
|
|
41
|
+
host,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default parseServerUrl;
|
|
@@ -0,0 +1,97 @@
|
|
|
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 { StyleSheet } from 'react-native';
|
|
18
|
+
|
|
19
|
+
const devStyles = StyleSheet.create({
|
|
20
|
+
screen: {
|
|
21
|
+
flexGrow: 1,
|
|
22
|
+
padding: 24,
|
|
23
|
+
gap: 12,
|
|
24
|
+
},
|
|
25
|
+
heading: {
|
|
26
|
+
fontSize: 24,
|
|
27
|
+
fontWeight: '600',
|
|
28
|
+
},
|
|
29
|
+
hint: {
|
|
30
|
+
fontSize: 14,
|
|
31
|
+
color: '#8c8c8c',
|
|
32
|
+
},
|
|
33
|
+
error: {
|
|
34
|
+
fontSize: 14,
|
|
35
|
+
color: '#cf1322',
|
|
36
|
+
},
|
|
37
|
+
input: {
|
|
38
|
+
borderWidth: 1,
|
|
39
|
+
borderColor: '#d9d9d9',
|
|
40
|
+
borderRadius: 8,
|
|
41
|
+
paddingHorizontal: 12,
|
|
42
|
+
paddingVertical: 10,
|
|
43
|
+
fontSize: 16,
|
|
44
|
+
},
|
|
45
|
+
button: {
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
paddingHorizontal: 20,
|
|
48
|
+
paddingVertical: 12,
|
|
49
|
+
borderRadius: 8,
|
|
50
|
+
backgroundColor: '#1677ff',
|
|
51
|
+
},
|
|
52
|
+
buttonText: {
|
|
53
|
+
color: '#ffffff',
|
|
54
|
+
fontSize: 16,
|
|
55
|
+
fontWeight: '600',
|
|
56
|
+
},
|
|
57
|
+
secondaryButton: {
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
paddingHorizontal: 20,
|
|
60
|
+
paddingVertical: 12,
|
|
61
|
+
borderRadius: 8,
|
|
62
|
+
backgroundColor: '#f0f0f0',
|
|
63
|
+
},
|
|
64
|
+
secondaryButtonText: {
|
|
65
|
+
fontSize: 16,
|
|
66
|
+
},
|
|
67
|
+
row: {
|
|
68
|
+
padding: 16,
|
|
69
|
+
borderRadius: 12,
|
|
70
|
+
backgroundColor: '#f0f0f0',
|
|
71
|
+
},
|
|
72
|
+
rowText: {
|
|
73
|
+
fontSize: 17,
|
|
74
|
+
},
|
|
75
|
+
backdrop: {
|
|
76
|
+
flex: 1,
|
|
77
|
+
justifyContent: 'flex-end',
|
|
78
|
+
backgroundColor: 'rgba(0,0,0,0.3)',
|
|
79
|
+
},
|
|
80
|
+
sheet: {
|
|
81
|
+
padding: 24,
|
|
82
|
+
gap: 12,
|
|
83
|
+
borderTopLeftRadius: 16,
|
|
84
|
+
borderTopRightRadius: 16,
|
|
85
|
+
backgroundColor: '#ffffff',
|
|
86
|
+
},
|
|
87
|
+
sheetTitle: {
|
|
88
|
+
fontSize: 20,
|
|
89
|
+
fontWeight: '600',
|
|
90
|
+
},
|
|
91
|
+
mono: {
|
|
92
|
+
fontFamily: 'Menlo',
|
|
93
|
+
fontSize: 12,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export default devStyles;
|
package/src/dev/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
// Lowdefy Dev's developer features: connecting to a dev server, the shake menu and the dev-tab
|
|
18
|
+
// client. A release shell's bundle resolves this module to an empty one (metro.config.js), so
|
|
19
|
+
// every export here is undefined there and the boot files fall back to the plain shell.
|
|
20
|
+
export { default as DevShell } from './DevShell.js';
|
|
21
|
+
export { default as redirectDevLink } from './redirectDevLink.js';
|
|
@@ -0,0 +1,188 @@
|
|
|
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 { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import injectPush from './injectPush.js';
|
|
20
|
+
import injectShare from './injectShare.js';
|
|
21
|
+
|
|
22
|
+
const NEXT_READ_KINDS = ['scan', 'photo', 'nfc', 'transcript'];
|
|
23
|
+
const PERMISSION_ANSWERS = ['granted', 'denied'];
|
|
24
|
+
const NETWORK_STATES = ['online', 'offline'];
|
|
25
|
+
const KINDS = [...NEXT_READ_KINDS, 'location', 'permission', 'network', 'push', 'share', 'clear'];
|
|
26
|
+
|
|
27
|
+
function describe(value) {
|
|
28
|
+
return JSON.stringify(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isTranscript(value) {
|
|
32
|
+
if (!type.isObject(value)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (!type.isUndefined(value.error)) {
|
|
36
|
+
return type.isString(value.error) && value.error !== '';
|
|
37
|
+
}
|
|
38
|
+
return (
|
|
39
|
+
type.isString(value.final) &&
|
|
40
|
+
(type.isUndefined(value.partials) ||
|
|
41
|
+
(type.isArray(value.partials) && value.partials.every(type.isString)))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isLocation(value) {
|
|
46
|
+
return type.isObject(value) && type.isNumber(value.latitude) && type.isNumber(value.longitude);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Checks one injection's value, throwing the message the agent gets back.
|
|
50
|
+
function validateInjection({ kind, name, value }) {
|
|
51
|
+
if (!KINDS.includes(kind)) {
|
|
52
|
+
throw new Error(`An injection kind is one of ${KINDS.join(', ')}. Received ${describe(kind)}.`);
|
|
53
|
+
}
|
|
54
|
+
if (kind === 'scan' && !(type.isObject(value) && type.isString(value.data))) {
|
|
55
|
+
throw new Error(`A scan injection needs { data }. Received ${describe(value)}.`);
|
|
56
|
+
}
|
|
57
|
+
if (kind === 'photo' && !(type.isObject(value) && type.isString(value.base64))) {
|
|
58
|
+
throw new Error(`A photo injection needs { base64 }. Received ${describe(value)}.`);
|
|
59
|
+
}
|
|
60
|
+
if (kind === 'nfc' && !type.isObject(value)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`An nfc injection needs the tag read as an object. Received ${describe(value)}.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (kind === 'transcript' && !isTranscript(value)) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`A transcript injection needs { final } with optional { partials } (a list of strings), or { error }. Received ${describe(
|
|
68
|
+
value
|
|
69
|
+
)}.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (kind === 'location') {
|
|
73
|
+
const route = type.isArray(value) ? value : [value];
|
|
74
|
+
if (route.length === 0 || !route.every(isLocation)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`A location injection needs { latitude, longitude }, or a route as a list of them. Received ${describe(
|
|
77
|
+
value
|
|
78
|
+
)}.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (kind === 'permission') {
|
|
83
|
+
if (!type.isString(name) || name === '') {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`A permission injection needs the permission's name. Received ${describe(name)}.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!PERMISSION_ANSWERS.includes(value)) {
|
|
89
|
+
throw new Error(`A permission answer is "granted" or "denied". Received ${describe(value)}.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (kind === 'network' && !NETWORK_STATES.includes(value)) {
|
|
93
|
+
throw new Error(`A network injection is "online" or "offline". Received ${describe(value)}.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The device results an agent or journey told Lowdefy Dev to fake, through the dev-tab client's
|
|
99
|
+
* `inject` requests. Device actions read them with `injected(name)` (src/device/injected.js), which
|
|
100
|
+
* calls take.
|
|
101
|
+
*
|
|
102
|
+
* - `scan`, `photo` and `nfc` queue: each read takes the next one, and none is left once taken.
|
|
103
|
+
* - `transcript` queues the same way: each dictation session takes the next one and "hears" its
|
|
104
|
+
* `partials`, then its `final`, or ends with its `error` (blocks-native's
|
|
105
|
+
* installDictationSource plays it).
|
|
106
|
+
* - `location` queues too, so a list of locations plays a route one read at a time, but the last
|
|
107
|
+
* one taken stays: the phone is still there for the next read and for geofences.
|
|
108
|
+
* - `permission` sets the standing answer for one permission, as the phone remembers a choice.
|
|
109
|
+
* - `network: 'offline'` makes the app's every fetch fail as offline (createOfflineFetch) and
|
|
110
|
+
* takes the shell offline (networkStatus), which shows the NoConnection banner and fires the
|
|
111
|
+
* app's `offline` events, until `network: 'online'`.
|
|
112
|
+
* - `clear` drops every injection and goes back online.
|
|
113
|
+
* - `push` shows a notification now, and can tap it or press a button (injectPush).
|
|
114
|
+
* - `share` shares text, a link or files into the app (injectShare).
|
|
115
|
+
*
|
|
116
|
+
* @param {Object} params
|
|
117
|
+
* @param {{ setInjectedOffline: (offline: boolean) => void }} params.networkStatus The shell's
|
|
118
|
+
* online state (host/networkStatus.js).
|
|
119
|
+
*/
|
|
120
|
+
function createDeviceInjections({ networkStatus }) {
|
|
121
|
+
let queues = new Map();
|
|
122
|
+
let standing = new Map();
|
|
123
|
+
let offline = false;
|
|
124
|
+
|
|
125
|
+
function setOffline(next) {
|
|
126
|
+
offline = next;
|
|
127
|
+
networkStatus.setInjectedOffline(next);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function queue(kind) {
|
|
131
|
+
if (!queues.has(kind)) {
|
|
132
|
+
queues.set(kind, []);
|
|
133
|
+
}
|
|
134
|
+
return queues.get(kind);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function inject({ kind, name, value }) {
|
|
138
|
+
validateInjection({ kind, name, value });
|
|
139
|
+
switch (kind) {
|
|
140
|
+
case 'clear':
|
|
141
|
+
queues = new Map();
|
|
142
|
+
standing = new Map();
|
|
143
|
+
setOffline(false);
|
|
144
|
+
return { ok: true, kind };
|
|
145
|
+
case 'network':
|
|
146
|
+
setOffline(value === 'offline');
|
|
147
|
+
return { ok: true, kind, network: value };
|
|
148
|
+
case 'permission':
|
|
149
|
+
standing.set(`permission:${name}`, value);
|
|
150
|
+
return { ok: true, kind, name, answer: value };
|
|
151
|
+
case 'location':
|
|
152
|
+
queue(kind).push(...(type.isArray(value) ? value : [value]));
|
|
153
|
+
return { ok: true, kind, pending: queue(kind).length };
|
|
154
|
+
case 'push':
|
|
155
|
+
return injectPush(value);
|
|
156
|
+
case 'share':
|
|
157
|
+
return injectShare(value);
|
|
158
|
+
default:
|
|
159
|
+
queue(kind).push(value);
|
|
160
|
+
return { ok: true, kind, pending: queue(kind).length };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function take(name) {
|
|
165
|
+
if (name.startsWith('permission:')) {
|
|
166
|
+
return standing.get(name);
|
|
167
|
+
}
|
|
168
|
+
if (name === 'location') {
|
|
169
|
+
const next = queue(name).shift();
|
|
170
|
+
if (next !== undefined) {
|
|
171
|
+
standing.set(name, next);
|
|
172
|
+
}
|
|
173
|
+
return standing.get(name);
|
|
174
|
+
}
|
|
175
|
+
if (!NEXT_READ_KINDS.includes(name)) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`injected() reads ${[...NEXT_READ_KINDS, 'location', 'permission:<name>'].join(
|
|
178
|
+
', '
|
|
179
|
+
)}. Received ${describe(name)}.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return queue(name).shift();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { inject, isOffline: () => offline, take };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export default createDeviceInjections;
|
|
@@ -0,0 +1,224 @@
|
|
|
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 createDeviceInjections from './createDeviceInjections.js';
|
|
18
|
+
import createFakeNetwork from '../../test/createFakeNetwork.js';
|
|
19
|
+
import createNetworkStatus from '../../host/createNetworkStatus.js';
|
|
20
|
+
|
|
21
|
+
function createInjections() {
|
|
22
|
+
const networkStatus = createNetworkStatus({ network: createFakeNetwork() });
|
|
23
|
+
return createDeviceInjections({ networkStatus });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fixtureImage = { base64: 'iVBORw0KGgo=', mimeType: 'image/png', width: 1, height: 1 };
|
|
27
|
+
|
|
28
|
+
test('scan, photo and nfc results are each taken once, in the order they were injected', () => {
|
|
29
|
+
const injections = createInjections();
|
|
30
|
+
expect(injections.inject({ kind: 'scan', value: { data: 'JOB-1', type: 'qr' } })).toEqual({
|
|
31
|
+
ok: true,
|
|
32
|
+
kind: 'scan',
|
|
33
|
+
pending: 1,
|
|
34
|
+
});
|
|
35
|
+
injections.inject({ kind: 'scan', value: { data: 'JOB-2', type: 'code128' } });
|
|
36
|
+
injections.inject({ kind: 'photo', value: fixtureImage });
|
|
37
|
+
injections.inject({ kind: 'nfc', value: { id: '04:A2:19', records: [] } });
|
|
38
|
+
expect(injections.take('scan')).toEqual({ data: 'JOB-1', type: 'qr' });
|
|
39
|
+
expect(injections.take('photo')).toEqual(fixtureImage);
|
|
40
|
+
expect(injections.take('scan')).toEqual({ data: 'JOB-2', type: 'code128' });
|
|
41
|
+
expect(injections.take('scan')).toBeUndefined();
|
|
42
|
+
expect(injections.take('photo')).toBeUndefined();
|
|
43
|
+
expect(injections.take('nfc')).toEqual({ id: '04:A2:19', records: [] });
|
|
44
|
+
expect(injections.take('nfc')).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('transcripts queue, each dictation session taking the next one, and clear drops them', () => {
|
|
48
|
+
const injections = createInjections();
|
|
49
|
+
expect(
|
|
50
|
+
injections.inject({
|
|
51
|
+
kind: 'transcript',
|
|
52
|
+
value: { partials: ['for', 'for Sam'], final: 'for Sam, can you look' },
|
|
53
|
+
})
|
|
54
|
+
).toEqual({ ok: true, kind: 'transcript', pending: 1 });
|
|
55
|
+
expect(injections.inject({ kind: 'transcript', value: { error: 'not-allowed' } })).toEqual({
|
|
56
|
+
ok: true,
|
|
57
|
+
kind: 'transcript',
|
|
58
|
+
pending: 2,
|
|
59
|
+
});
|
|
60
|
+
injections.inject({ kind: 'permission', name: 'microphone', value: 'denied' });
|
|
61
|
+
expect(injections.take('transcript')).toEqual({
|
|
62
|
+
partials: ['for', 'for Sam'],
|
|
63
|
+
final: 'for Sam, can you look',
|
|
64
|
+
});
|
|
65
|
+
expect(injections.take('permission:microphone')).toBe('denied');
|
|
66
|
+
expect(injections.take('transcript')).toEqual({ error: 'not-allowed' });
|
|
67
|
+
expect(injections.take('transcript')).toBeUndefined();
|
|
68
|
+
injections.inject({ kind: 'transcript', value: { final: 'ship it' } });
|
|
69
|
+
injections.inject({ kind: 'clear' });
|
|
70
|
+
expect(injections.take('transcript')).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('a location stays after it is read, and a route plays one location per read', () => {
|
|
74
|
+
const injections = createInjections();
|
|
75
|
+
expect(injections.take('location')).toBeUndefined();
|
|
76
|
+
injections.inject({ kind: 'location', value: { latitude: -33.92, longitude: 18.42 } });
|
|
77
|
+
expect(injections.take('location')).toEqual({ latitude: -33.92, longitude: 18.42 });
|
|
78
|
+
expect(injections.take('location')).toEqual({ latitude: -33.92, longitude: 18.42 });
|
|
79
|
+
expect(
|
|
80
|
+
injections.inject({
|
|
81
|
+
kind: 'location',
|
|
82
|
+
value: [
|
|
83
|
+
{ latitude: -33.9, longitude: 18.4 },
|
|
84
|
+
{ latitude: -33.8, longitude: 18.5, accuracy: 5 },
|
|
85
|
+
],
|
|
86
|
+
})
|
|
87
|
+
).toEqual({ ok: true, kind: 'location', pending: 2 });
|
|
88
|
+
expect(injections.take('location')).toEqual({ latitude: -33.9, longitude: 18.4 });
|
|
89
|
+
expect(injections.take('location')).toEqual({ latitude: -33.8, longitude: 18.5, accuracy: 5 });
|
|
90
|
+
expect(injections.take('location')).toEqual({ latitude: -33.8, longitude: 18.5, accuracy: 5 });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('a permission answer stands for that permission until it changes', () => {
|
|
94
|
+
const injections = createInjections();
|
|
95
|
+
expect(injections.take('permission:camera')).toBeUndefined();
|
|
96
|
+
injections.inject({ kind: 'permission', name: 'camera', value: 'denied' });
|
|
97
|
+
expect(injections.take('permission:camera')).toBe('denied');
|
|
98
|
+
expect(injections.take('permission:camera')).toBe('denied');
|
|
99
|
+
expect(injections.take('permission:location')).toBeUndefined();
|
|
100
|
+
injections.inject({ kind: 'permission', name: 'camera', value: 'granted' });
|
|
101
|
+
expect(injections.take('permission:camera')).toBe('granted');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('network offline holds until the network is turned back on', () => {
|
|
105
|
+
const injections = createInjections();
|
|
106
|
+
expect(injections.isOffline()).toBe(false);
|
|
107
|
+
expect(injections.inject({ kind: 'network', value: 'offline' })).toEqual({
|
|
108
|
+
ok: true,
|
|
109
|
+
kind: 'network',
|
|
110
|
+
network: 'offline',
|
|
111
|
+
});
|
|
112
|
+
expect(injections.isOffline()).toBe(true);
|
|
113
|
+
injections.inject({ kind: 'network', value: 'online' });
|
|
114
|
+
expect(injections.isOffline()).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// The NoConnection banner and the app's online and offline events read the shell's online state.
|
|
118
|
+
test('network offline takes the shell offline, and online or clear brings it back', () => {
|
|
119
|
+
const networkStatus = createNetworkStatus({ network: createFakeNetwork() });
|
|
120
|
+
const injections = createDeviceInjections({ networkStatus });
|
|
121
|
+
injections.inject({ kind: 'network', value: 'offline' });
|
|
122
|
+
expect(networkStatus.isOnline()).toBe(false);
|
|
123
|
+
injections.inject({ kind: 'network', value: 'online' });
|
|
124
|
+
expect(networkStatus.isOnline()).toBe(true);
|
|
125
|
+
injections.inject({ kind: 'network', value: 'offline' });
|
|
126
|
+
injections.inject({ kind: 'clear' });
|
|
127
|
+
expect(networkStatus.isOnline()).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('clear drops every injection and goes back online', () => {
|
|
131
|
+
const injections = createInjections();
|
|
132
|
+
injections.inject({ kind: 'scan', value: { data: 'JOB-1' } });
|
|
133
|
+
injections.inject({ kind: 'location', value: { latitude: 1, longitude: 2 } });
|
|
134
|
+
injections.take('location');
|
|
135
|
+
injections.inject({ kind: 'permission', name: 'location', value: 'denied' });
|
|
136
|
+
injections.inject({ kind: 'network', value: 'offline' });
|
|
137
|
+
expect(injections.inject({ kind: 'clear' })).toEqual({ ok: true, kind: 'clear' });
|
|
138
|
+
expect(injections.take('scan')).toBeUndefined();
|
|
139
|
+
expect(injections.take('location')).toBeUndefined();
|
|
140
|
+
expect(injections.take('permission:location')).toBeUndefined();
|
|
141
|
+
expect(injections.isOffline()).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('a push injection without a title or body is refused with its shape', async () => {
|
|
145
|
+
const injections = createInjections();
|
|
146
|
+
await expect(injections.inject({ kind: 'push', value: { data: {} } })).rejects.toThrow(
|
|
147
|
+
'A push injection needs { title, body, data?, categoryId?, open?, actionId?, text? }.'
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('a push injection shows the notification now', async () => {
|
|
152
|
+
const injections = createInjections();
|
|
153
|
+
await expect(injections.inject({ kind: 'push', value: { title: 'New job' } })).resolves.toEqual({
|
|
154
|
+
ok: true,
|
|
155
|
+
kind: 'push',
|
|
156
|
+
id: 'notification-id',
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('a share injection without text, a url or files is refused', async () => {
|
|
161
|
+
const injections = createInjections();
|
|
162
|
+
await expect(injections.inject({ kind: 'share', value: {} })).rejects.toThrow(
|
|
163
|
+
'A share injection needs { text?, url?, files? }.'
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test.each([
|
|
168
|
+
[
|
|
169
|
+
{ kind: 'beep' },
|
|
170
|
+
'An injection kind is one of scan, photo, nfc, transcript, location, permission, network, push, share, clear. Received "beep".',
|
|
171
|
+
],
|
|
172
|
+
[{ kind: 'scan', value: 'JOB-1' }, 'A scan injection needs { data }. Received "JOB-1".'],
|
|
173
|
+
[{ kind: 'photo', value: {} }, 'A photo injection needs { base64 }. Received {}.'],
|
|
174
|
+
[
|
|
175
|
+
{ kind: 'nfc', value: 'tag' },
|
|
176
|
+
'An nfc injection needs the tag read as an object. Received "tag".',
|
|
177
|
+
],
|
|
178
|
+
[
|
|
179
|
+
{ kind: 'transcript', value: 'for Sam' },
|
|
180
|
+
'A transcript injection needs { final } with optional { partials } (a list of strings), or { error }. Received "for Sam".',
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
{ kind: 'transcript', value: { partials: ['for'] } },
|
|
184
|
+
'A transcript injection needs { final } with optional { partials } (a list of strings), or { error }. Received {"partials":["for"]}.',
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
{ kind: 'transcript', value: { partials: 'for', final: 'for Sam' } },
|
|
188
|
+
'A transcript injection needs { final } with optional { partials } (a list of strings), or { error }. Received {"partials":"for","final":"for Sam"}.',
|
|
189
|
+
],
|
|
190
|
+
[
|
|
191
|
+
{ kind: 'transcript', value: { error: '' } },
|
|
192
|
+
'A transcript injection needs { final } with optional { partials } (a list of strings), or { error }. Received {"error":""}.',
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
{ kind: 'location', value: { latitude: '1', longitude: 2 } },
|
|
196
|
+
'A location injection needs { latitude, longitude }, or a route as a list of them. Received {"latitude":"1","longitude":2}.',
|
|
197
|
+
],
|
|
198
|
+
[
|
|
199
|
+
{ kind: 'location', value: [] },
|
|
200
|
+
'A location injection needs { latitude, longitude }, or a route as a list of them. Received [].',
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
{ kind: 'permission', value: 'granted' },
|
|
204
|
+
"A permission injection needs the permission's name. Received undefined.",
|
|
205
|
+
],
|
|
206
|
+
[
|
|
207
|
+
{ kind: 'permission', name: 'camera', value: 'yes' },
|
|
208
|
+
'A permission answer is "granted" or "denied". Received "yes".',
|
|
209
|
+
],
|
|
210
|
+
[
|
|
211
|
+
{ kind: 'network', value: false },
|
|
212
|
+
'A network injection is "online" or "offline". Received false.',
|
|
213
|
+
],
|
|
214
|
+
])('an invalid injection is refused with what was wrong: %j', (injection, message) => {
|
|
215
|
+
const injections = createInjections();
|
|
216
|
+
expect(() => injections.inject(injection)).toThrow(message);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('reading an injection by a name no device action uses throws', () => {
|
|
220
|
+
const injections = createInjections();
|
|
221
|
+
expect(() => injections.take('barcode')).toThrow(
|
|
222
|
+
'injected() reads scan, photo, nfc, transcript, location, permission:<name>. Received "barcode".'
|
|
223
|
+
);
|
|
224
|
+
});
|
|
@@ -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
|
+
/**
|
|
18
|
+
* The app's fetch while Lowdefy Dev pretends to be offline (createDeviceInjections): every call
|
|
19
|
+
* fails as React Native's fetch fails with no network. The dev-tab client keeps its own fetch, so
|
|
20
|
+
* an agent can still turn the network back on.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} params
|
|
23
|
+
* @param {Function} params.fetch The Fetch API.
|
|
24
|
+
* @param {() => boolean} params.isOffline
|
|
25
|
+
*/
|
|
26
|
+
function createOfflineFetch({ fetch, isOffline }) {
|
|
27
|
+
return function offlineFetch(url, init) {
|
|
28
|
+
if (isOffline()) {
|
|
29
|
+
return Promise.reject(new TypeError('Network request failed'));
|
|
30
|
+
}
|
|
31
|
+
return fetch(url, init);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default createOfflineFetch;
|