@lowdefy/shell 3.23.2 → 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.bde87d1433f0625faed6.js +0 -1
- package/dist/shell/runtime.1ead93159ba56c05b969.js +0 -1
|
@@ -0,0 +1,68 @@
|
|
|
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 { jest } from '@jest/globals';
|
|
18
|
+
|
|
19
|
+
import createDevShellConfig from '../connection/createDevShellConfig.js';
|
|
20
|
+
import createDeviceInjections from './createDeviceInjections.js';
|
|
21
|
+
import createFakeNetwork from '../../test/createFakeNetwork.js';
|
|
22
|
+
import createNetworkStatus from '../../host/createNetworkStatus.js';
|
|
23
|
+
import createOfflineFetch from './createOfflineFetch.js';
|
|
24
|
+
|
|
25
|
+
const shell = { environment: 'dev', version: '7.4.0' };
|
|
26
|
+
|
|
27
|
+
function setup({ pair = null } = {}) {
|
|
28
|
+
const injections = createDeviceInjections({
|
|
29
|
+
networkStatus: createNetworkStatus({ network: createFakeNetwork() }),
|
|
30
|
+
});
|
|
31
|
+
const fetch = jest.fn(async () => ({ ok: true }));
|
|
32
|
+
const devShell = createDevShellConfig({
|
|
33
|
+
fetch: createOfflineFetch({ fetch, isOffline: injections.isOffline }),
|
|
34
|
+
server: { url: 'http://192.168.1.20:3000', host: '192.168.1.20:3000', pair },
|
|
35
|
+
shell,
|
|
36
|
+
WebSocket: class {},
|
|
37
|
+
});
|
|
38
|
+
return { devShell, fetch, injections };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("offline, the app's every fetch fails as React Native's fetch fails with no network", async () => {
|
|
42
|
+
const { devShell, fetch, injections } = setup({ pair: 'token' });
|
|
43
|
+
await devShell.transport.fetch('http://192.168.1.20:3000/api/page/home');
|
|
44
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
45
|
+
|
|
46
|
+
injections.inject({ kind: 'network', value: 'offline' });
|
|
47
|
+
const failed = devShell.transport.fetch('http://192.168.1.20:3000/api/request/home/load');
|
|
48
|
+
await expect(failed).rejects.toThrow(TypeError);
|
|
49
|
+
await expect(failed).rejects.toThrow('Network request failed');
|
|
50
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
51
|
+
|
|
52
|
+
injections.inject({ kind: 'network', value: 'online' });
|
|
53
|
+
await devShell.transport.fetch('http://192.168.1.20:3000/api/page/home');
|
|
54
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('offline, the dev-tab client still reaches the dev server, so an agent can go back online', async () => {
|
|
58
|
+
const { devShell, injections } = setup({ pair: 'token' });
|
|
59
|
+
injections.inject({ kind: 'network', value: 'offline' });
|
|
60
|
+
const tabFetch = jest.fn(async () => ({ ok: true }));
|
|
61
|
+
await devShell.transport.wrapFetch(tabFetch)('http://192.168.1.20:3000/api/dev-inspect', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
});
|
|
64
|
+
expect(tabFetch).toHaveBeenCalledWith('http://192.168.1.20:3000/api/dev-inspect', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'x-lowdefy-pair': 'token' },
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
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 * as Notifications from 'expo-notifications';
|
|
18
|
+
import { type } from '@lowdefy/helpers';
|
|
19
|
+
|
|
20
|
+
import { getCurrentApp } from '../../native/currentApp.js';
|
|
21
|
+
import handleNotificationResponse from '../../notifications/handleNotificationResponse.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Lowdefy Dev's push injection: shows `{ title, body, data, categoryId }` as a notification now,
|
|
25
|
+
* as a push arriving with the app open would (the app's onNotification fires). With `open: true`
|
|
26
|
+
* it is then tapped (data.path opens, onNotificationAction fires with actionId default); with
|
|
27
|
+
* `actionId` (and `text` for a reply) that button is pressed instead, as the person would.
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} value
|
|
30
|
+
*/
|
|
31
|
+
async function injectPush(value) {
|
|
32
|
+
if (!type.isObject(value) || !(type.isString(value.title) || type.isString(value.body))) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`A push injection needs { title, body, data?, categoryId?, open?, actionId?, text? }. Received ${JSON.stringify(
|
|
35
|
+
value
|
|
36
|
+
)}.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const content = {
|
|
40
|
+
title: value.title,
|
|
41
|
+
body: value.body,
|
|
42
|
+
data: value.data ?? {},
|
|
43
|
+
categoryIdentifier: value.categoryId,
|
|
44
|
+
};
|
|
45
|
+
const id = await Notifications.scheduleNotificationAsync({ content, trigger: null });
|
|
46
|
+
if (value.open !== true && !type.isString(value.actionId)) {
|
|
47
|
+
return { ok: true, kind: 'push', id };
|
|
48
|
+
}
|
|
49
|
+
await handleNotificationResponse({
|
|
50
|
+
app: await getCurrentApp(),
|
|
51
|
+
response: {
|
|
52
|
+
actionIdentifier: value.actionId ?? Notifications.DEFAULT_ACTION_IDENTIFIER,
|
|
53
|
+
notification: { request: { identifier: id, content } },
|
|
54
|
+
userText: value.text,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return { ok: true, kind: 'push', id, responded: value.actionId ?? 'default' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default injectPush;
|
|
@@ -0,0 +1,55 @@
|
|
|
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 { getCurrentApp } from '../../native/currentApp.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Lowdefy Dev's share injection: as if `{ text?, url?, files? }` were shared into the app from
|
|
23
|
+
* another app, so mobile.share.pageId opens with it and onShare fires. Files are
|
|
24
|
+
* [{ uri, name, mimeType, size? }].
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} value
|
|
27
|
+
*/
|
|
28
|
+
async function injectShare(value) {
|
|
29
|
+
if (
|
|
30
|
+
!type.isObject(value) ||
|
|
31
|
+
![value.text, value.url, value.files].some((item) => !type.isNone(item))
|
|
32
|
+
) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`A share injection needs { text?, url?, files? }. Received ${JSON.stringify(value)}.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const payloads = [
|
|
38
|
+
...(type.isString(value.text) ? [{ shareType: 'text', value: value.text }] : []),
|
|
39
|
+
...(type.isString(value.url) ? [{ shareType: 'url', value: value.url }] : []),
|
|
40
|
+
...(value.files ?? []).map((file) => ({
|
|
41
|
+
shareType: 'file',
|
|
42
|
+
value: file.uri,
|
|
43
|
+
contentUri: file.uri,
|
|
44
|
+
contentType: 'file',
|
|
45
|
+
contentMimeType: file.mimeType ?? null,
|
|
46
|
+
originalName: file.name ?? null,
|
|
47
|
+
contentSize: file.size ?? null,
|
|
48
|
+
})),
|
|
49
|
+
];
|
|
50
|
+
const app = await getCurrentApp();
|
|
51
|
+
await app.receiveShare({ payloads });
|
|
52
|
+
return { ok: true, kind: 'share' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default injectShare;
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { FEATURE_SHELLS } from '@lowdefy/blocks-native/device';
|
|
18
|
+
|
|
19
|
+
// An injected photo as a picked file: the PickImage shape, with its content inline.
|
|
20
|
+
function toPhotoFile(photo) {
|
|
21
|
+
const mimeType = photo.mimeType ?? 'image/jpeg';
|
|
22
|
+
return {
|
|
23
|
+
name: photo.name ?? 'injected-photo.jpg',
|
|
24
|
+
mimeType,
|
|
25
|
+
size: Math.round((photo.base64.length * 3) / 4),
|
|
26
|
+
uri: `data:${mimeType};base64,${photo.base64}`,
|
|
27
|
+
width: photo.width ?? null,
|
|
28
|
+
height: photo.height ?? null,
|
|
29
|
+
base64: photo.base64,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A feature method that answers an injected result first, and asks the shell's own feature
|
|
34
|
+
// otherwise. Lowdefy Dev carries the field profile, so NFC has no feature of its own there: an
|
|
35
|
+
// injection is then the only answer, and a read with none throws as a shell without NFC does.
|
|
36
|
+
function answerFirst({ action, feature, method, read, real, toResult }) {
|
|
37
|
+
return async (params) => {
|
|
38
|
+
const value = read();
|
|
39
|
+
if (value !== undefined) {
|
|
40
|
+
return toResult(value);
|
|
41
|
+
}
|
|
42
|
+
if (real === undefined) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`${action} needs a shell that carries ${feature} (${FEATURE_SHELLS[feature]}). In Lowdefy Dev, inject one.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return real[method](params);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Lowdefy Dev's device: the shell's features, with the next scan, photo and NFC read answered by
|
|
53
|
+
* what an agent or journey injected (src/device/injected.js) before the camera or the tag reader.
|
|
54
|
+
*
|
|
55
|
+
* @param {Object} params
|
|
56
|
+
* @param {Object} params.features The shell's device features (src/device/device.js).
|
|
57
|
+
* @param {(name: string) => any} params.read injected().
|
|
58
|
+
*/
|
|
59
|
+
function withInjectedDevice({ features, read }) {
|
|
60
|
+
const photo = (method, feature, action) =>
|
|
61
|
+
answerFirst({
|
|
62
|
+
action,
|
|
63
|
+
feature,
|
|
64
|
+
method,
|
|
65
|
+
read: () => read('photo'),
|
|
66
|
+
real: features[feature],
|
|
67
|
+
toResult: (value) => [toPhotoFile(value)],
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
...features,
|
|
71
|
+
camera: {
|
|
72
|
+
...features.camera,
|
|
73
|
+
scanCode: answerFirst({
|
|
74
|
+
action: 'ScanCode',
|
|
75
|
+
feature: 'camera',
|
|
76
|
+
method: 'scanCode',
|
|
77
|
+
read: () => read('scan'),
|
|
78
|
+
real: features.camera,
|
|
79
|
+
toResult: (value) => ({ data: value.data, type: value.type ?? 'qr' }),
|
|
80
|
+
}),
|
|
81
|
+
takePhoto: photo('takePhoto', 'camera', 'TakePhoto'),
|
|
82
|
+
},
|
|
83
|
+
photos: { ...features.photos, pick: photo('pick', 'photos', 'PickImage') },
|
|
84
|
+
nfc: {
|
|
85
|
+
...features.nfc,
|
|
86
|
+
read: answerFirst({
|
|
87
|
+
action: 'ReadNfc',
|
|
88
|
+
feature: 'nfc',
|
|
89
|
+
method: 'read',
|
|
90
|
+
read: () => read('nfc'),
|
|
91
|
+
real: features.nfc,
|
|
92
|
+
toResult: (value) => value,
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default withInjectedDevice;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { Text, View } from 'react-native';
|
|
19
|
+
|
|
20
|
+
import devStyles from '../devStyles.js';
|
|
21
|
+
|
|
22
|
+
// The shake menu's block gallery: every native block the shell carries, shown with its examples.
|
|
23
|
+
// The native block set fills it in; until then it says so.
|
|
24
|
+
function BlockGallery() {
|
|
25
|
+
return (
|
|
26
|
+
<View style={devStyles.screen} testID="dev-block-gallery">
|
|
27
|
+
<Text style={devStyles.heading}>Block gallery</Text>
|
|
28
|
+
<Text style={devStyles.hint}>The gallery of native blocks is not in this build yet.</Text>
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default BlockGallery;
|
|
@@ -0,0 +1,121 @@
|
|
|
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, useSyncExternalStore } from 'react';
|
|
18
|
+
import { Modal, Pressable, ScrollView, Text, View } from 'react-native';
|
|
19
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
20
|
+
|
|
21
|
+
import BlockGallery from './BlockGallery.js';
|
|
22
|
+
import devStyles from '../devStyles.js';
|
|
23
|
+
|
|
24
|
+
function MenuItem({ hint, label, onPress, testID }) {
|
|
25
|
+
return (
|
|
26
|
+
<Pressable accessibilityRole="button" onPress={onPress} style={devStyles.row} testID={testID}>
|
|
27
|
+
<Text style={devStyles.rowText}>{label}</Text>
|
|
28
|
+
{hint ? <Text style={devStyles.hint}>{hint}</Text> : null}
|
|
29
|
+
</Pressable>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function LogList({ logs }) {
|
|
34
|
+
const entries = useSyncExternalStore(logs.subscribe, logs.entries);
|
|
35
|
+
if (entries.length === 0) {
|
|
36
|
+
return <Text style={devStyles.hint}>Nothing logged yet.</Text>;
|
|
37
|
+
}
|
|
38
|
+
return entries.map((entry, index) => (
|
|
39
|
+
<Text key={index} style={devStyles.mono}>
|
|
40
|
+
{`${entry.at.toISOString().slice(11, 19)} ${entry.level} ${entry.message}`}
|
|
41
|
+
</Text>
|
|
42
|
+
));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The shake menu: reload, switch server, clear data, the shell's logs, the block gallery and the
|
|
47
|
+
* shell's version.
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} props
|
|
50
|
+
* @param {boolean} props.visible
|
|
51
|
+
* @param {() => void} props.onClose
|
|
52
|
+
* @param {() => void} props.onReload
|
|
53
|
+
* @param {() => void} props.onSwitchServer
|
|
54
|
+
* @param {() => void} props.onClearData
|
|
55
|
+
* @param {Object} props.logs The log buffer (createLogBuffer).
|
|
56
|
+
* @param {string|null} props.server The host of the server Lowdefy Dev is connected to.
|
|
57
|
+
* @param {Object} props.shell The shell config: name and version.
|
|
58
|
+
*/
|
|
59
|
+
function DevMenu({ logs, onClearData, onClose, onReload, onSwitchServer, server, shell, visible }) {
|
|
60
|
+
const insets = useSafeAreaInsets();
|
|
61
|
+
const [panel, setPanel] = useState('menu');
|
|
62
|
+
function close() {
|
|
63
|
+
setPanel('menu');
|
|
64
|
+
onClose();
|
|
65
|
+
}
|
|
66
|
+
function act(action) {
|
|
67
|
+
close();
|
|
68
|
+
action();
|
|
69
|
+
}
|
|
70
|
+
return (
|
|
71
|
+
<Modal animationType="slide" onRequestClose={close} transparent visible={visible}>
|
|
72
|
+
<View style={devStyles.backdrop}>
|
|
73
|
+
<ScrollView
|
|
74
|
+
contentContainerStyle={[devStyles.sheet, { paddingBottom: insets.bottom + 24 }]}
|
|
75
|
+
style={{ maxHeight: '85%', flexGrow: 0 }}
|
|
76
|
+
testID="dev-menu"
|
|
77
|
+
>
|
|
78
|
+
{panel === 'menu' ? (
|
|
79
|
+
<>
|
|
80
|
+
<Text style={devStyles.sheetTitle}>{shell.name}</Text>
|
|
81
|
+
<Text style={devStyles.hint} testID="dev-menu-version">
|
|
82
|
+
{`Shell ${shell.version}${server ? ` · ${server}` : ''}`}
|
|
83
|
+
</Text>
|
|
84
|
+
<MenuItem label="Reload" onPress={() => act(onReload)} testID="dev-menu-reload" />
|
|
85
|
+
<MenuItem
|
|
86
|
+
hint={server ?? undefined}
|
|
87
|
+
label="Switch server"
|
|
88
|
+
onPress={() => act(onSwitchServer)}
|
|
89
|
+
testID="dev-menu-switch"
|
|
90
|
+
/>
|
|
91
|
+
<MenuItem
|
|
92
|
+
hint="Signs out and removes this server's cached pages and storage"
|
|
93
|
+
label="Clear data"
|
|
94
|
+
onPress={() => act(onClearData)}
|
|
95
|
+
testID="dev-menu-clear"
|
|
96
|
+
/>
|
|
97
|
+
<MenuItem label="Show logs" onPress={() => setPanel('logs')} testID="dev-menu-logs" />
|
|
98
|
+
<MenuItem
|
|
99
|
+
label="Block gallery"
|
|
100
|
+
onPress={() => setPanel('gallery')}
|
|
101
|
+
testID="dev-menu-gallery"
|
|
102
|
+
/>
|
|
103
|
+
</>
|
|
104
|
+
) : null}
|
|
105
|
+
{panel === 'logs' ? <LogList logs={logs} /> : null}
|
|
106
|
+
{panel === 'gallery' ? <BlockGallery /> : null}
|
|
107
|
+
<Pressable
|
|
108
|
+
accessibilityRole="button"
|
|
109
|
+
onPress={panel === 'menu' ? close : () => setPanel('menu')}
|
|
110
|
+
style={devStyles.secondaryButton}
|
|
111
|
+
testID="dev-menu-close"
|
|
112
|
+
>
|
|
113
|
+
<Text style={devStyles.secondaryButtonText}>{panel === 'menu' ? 'Close' : 'Back'}</Text>
|
|
114
|
+
</Pressable>
|
|
115
|
+
</ScrollView>
|
|
116
|
+
</View>
|
|
117
|
+
</Modal>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default DevMenu;
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
const LEVELS = ['log', 'info', 'warn', 'error'];
|
|
18
|
+
|
|
19
|
+
function formatPart(part) {
|
|
20
|
+
if (part instanceof Error) {
|
|
21
|
+
return part.message;
|
|
22
|
+
}
|
|
23
|
+
if (typeof part === 'string') {
|
|
24
|
+
return part;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return JSON.stringify(part);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return String(part);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The shell's recent console output, for the shake menu's "Show logs": the last `limit` lines,
|
|
35
|
+
* `{ at, level, message }`, oldest first. install() copies every console call into it (the console
|
|
36
|
+
* still prints); it returns the uninstall function.
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} params
|
|
39
|
+
* @param {Object} params.console The console to follow.
|
|
40
|
+
* @param {number} [params.limit]
|
|
41
|
+
*/
|
|
42
|
+
function createLogBuffer({ console, limit = 200 }) {
|
|
43
|
+
// Replaced, never changed, on each line: a snapshot for useSyncExternalStore.
|
|
44
|
+
let entries = [];
|
|
45
|
+
const listeners = new Set();
|
|
46
|
+
|
|
47
|
+
function add({ level, parts }) {
|
|
48
|
+
const entry = { at: new Date(), level, message: parts.map(formatPart).join(' ') };
|
|
49
|
+
entries = [...entries, entry].slice(-limit);
|
|
50
|
+
// A component may log while React renders another: the log view hears of it afterwards.
|
|
51
|
+
setTimeout(() => listeners.forEach((listener) => listener()), 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function install() {
|
|
55
|
+
const originals = {};
|
|
56
|
+
LEVELS.forEach((level) => {
|
|
57
|
+
originals[level] = console[level];
|
|
58
|
+
console[level] = (...parts) => {
|
|
59
|
+
add({ level, parts });
|
|
60
|
+
originals[level].apply(console, parts);
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
LEVELS.forEach((level) => {
|
|
65
|
+
console[level] = originals[level];
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function subscribe(listener) {
|
|
71
|
+
listeners.add(listener);
|
|
72
|
+
return () => listeners.delete(listener);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
add: (level, ...parts) => add({ level, parts }),
|
|
77
|
+
entries: () => entries,
|
|
78
|
+
install,
|
|
79
|
+
subscribe,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default createLogBuffer;
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
const UPDATE_MS = 100;
|
|
18
|
+
// In g: a phone at rest reads about 1.
|
|
19
|
+
const SHAKE_FORCE = 1.8;
|
|
20
|
+
const JOLTS = 3;
|
|
21
|
+
const WINDOW_MS = 800;
|
|
22
|
+
const QUIET_MS = 1500;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Calls onShake when the phone is shaken: 3 jolts over 1.8 g within 0.8 s, then not again for 1.5 s.
|
|
26
|
+
* Returns the function that stops listening.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} params
|
|
29
|
+
* @param {Object} params.accelerometer expo-sensors' Accelerometer.
|
|
30
|
+
* @param {() => void} params.onShake
|
|
31
|
+
* @param {() => number} [params.now]
|
|
32
|
+
*/
|
|
33
|
+
function watchShake({ accelerometer, now = Date.now, onShake }) {
|
|
34
|
+
let jolts = [];
|
|
35
|
+
let quietUntil = 0;
|
|
36
|
+
accelerometer.setUpdateInterval(UPDATE_MS);
|
|
37
|
+
const subscription = accelerometer.addListener(({ x, y, z }) => {
|
|
38
|
+
const time = now();
|
|
39
|
+
if (time < quietUntil || Math.sqrt(x * x + y * y + z * z) < SHAKE_FORCE) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
jolts = [...jolts.filter((at) => time - at <= WINDOW_MS), time];
|
|
43
|
+
if (jolts.length >= JOLTS) {
|
|
44
|
+
jolts = [];
|
|
45
|
+
quietUntil = time + QUIET_MS;
|
|
46
|
+
onShake();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return () => subscription.remove();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default watchShake;
|
|
@@ -0,0 +1,61 @@
|
|
|
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 { jest } from '@jest/globals';
|
|
18
|
+
|
|
19
|
+
import watchShake from './watchShake.js';
|
|
20
|
+
|
|
21
|
+
function setup() {
|
|
22
|
+
let listener = null;
|
|
23
|
+
let time = 0;
|
|
24
|
+
const onShake = jest.fn();
|
|
25
|
+
const stop = watchShake({
|
|
26
|
+
accelerometer: {
|
|
27
|
+
setUpdateInterval: jest.fn(),
|
|
28
|
+
addListener: (fn) => {
|
|
29
|
+
listener = fn;
|
|
30
|
+
return { remove: jest.fn() };
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
now: () => time,
|
|
34
|
+
onShake,
|
|
35
|
+
});
|
|
36
|
+
function read(g, at) {
|
|
37
|
+
time = at;
|
|
38
|
+
listener({ x: g, y: 0, z: 0 });
|
|
39
|
+
}
|
|
40
|
+
return { onShake, read, stop };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('three jolts within 0.8 s open the menu once, then it rests for 1.5 s', () => {
|
|
44
|
+
const { onShake, read } = setup();
|
|
45
|
+
read(2.5, 0);
|
|
46
|
+
read(2.5, 200);
|
|
47
|
+
expect(onShake).not.toHaveBeenCalled();
|
|
48
|
+
read(2.5, 400);
|
|
49
|
+
expect(onShake).toHaveBeenCalledTimes(1);
|
|
50
|
+
[500, 600, 700].forEach((at) => read(2.5, at));
|
|
51
|
+
expect(onShake).toHaveBeenCalledTimes(1);
|
|
52
|
+
[2000, 2100, 2200].forEach((at) => read(2.5, at));
|
|
53
|
+
expect(onShake).toHaveBeenCalledTimes(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('a phone at rest, or jolts spread out, do not open the menu', () => {
|
|
57
|
+
const { onShake, read } = setup();
|
|
58
|
+
[0, 100, 200, 300].forEach((at) => read(1, at));
|
|
59
|
+
[1000, 2000, 3000].forEach((at) => read(2.5, at));
|
|
60
|
+
expect(onShake).not.toHaveBeenCalled();
|
|
61
|
+
});
|