@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
package/README.md
CHANGED
|
@@ -1,5 +1,367 @@
|
|
|
1
1
|
# @lowdefy/shell
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Expo app every Lowdefy phone app is built from (Expo SDK 58, React Native 0.88, Hermes, New
|
|
4
|
+
Architecture). It carries code only: the Lowdefy engine, `@lowdefy/client-native` and the shell's
|
|
5
|
+
plugin maps. Pages, menus and theme come from the app's Lowdefy server as data. No `ios/` or
|
|
6
|
+
`android/` folder is committed: Expo writes them at build time.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
## Config
|
|
9
|
+
|
|
10
|
+
`app.config.ts` reads `shell.json` from the path in `LOWDEFY_SHELL_CONFIG`, and falls back to
|
|
11
|
+
`shell.dev.json` (Lowdefy Dev):
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"id": "field",
|
|
16
|
+
"name": "Field",
|
|
17
|
+
"profile": "field",
|
|
18
|
+
"version": "1.4.2",
|
|
19
|
+
"icon": "./icon.png",
|
|
20
|
+
"color": "#0B5FFF",
|
|
21
|
+
"scheme": "field",
|
|
22
|
+
"environment": "production",
|
|
23
|
+
"eas": { "owner": "example", "projectId": "2b6f…" },
|
|
24
|
+
"ios": { "bundleIdentifier": "com.example.field" },
|
|
25
|
+
"android": { "package": "com.example.field" },
|
|
26
|
+
"store": { "ios": "https://apps.apple.com/...", "android": "https://play.google.com/..." },
|
|
27
|
+
"app": { "id": "field", "name": "Field", "urls": { "production": "https://field.example.com" } }
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
A shell opens exactly one app, named by `app`. Every app's `_js` runs in the shell's one JavaScript
|
|
32
|
+
realm, which cannot be sandboxed, so a second app in the same shell could reach the first one's
|
|
33
|
+
session token, storage and cache. `checkShellConfig.js` refuses a `shell.json` that lists `apps`,
|
|
34
|
+
names more than one app, or has no url for the shell's `environment`; `app.config.ts` runs it, so
|
|
35
|
+
`expo prebuild` and `expo export` fail on such a config.
|
|
36
|
+
|
|
37
|
+
`profile` names the shell's capability profile, one row of the table in `profiles.js`: `basic`,
|
|
38
|
+
`field`, `field-nfc`, `field-tracking`, and `dev` and `development` for Lowdefy Dev and development
|
|
39
|
+
builds (see Native features for what each carries). Each lists the native modules, config plugins, permissions, JavaScript entry modules,
|
|
40
|
+
required-reason APIs and collected data it carries, and may extend another (`field` is `basic` and
|
|
41
|
+
the camera). `basic`, and so every profile, carries on-device dictation (`expo-speech-recognition`)
|
|
42
|
+
with its microphone and speech purpose texts; `expo-camera` is given the same microphone text, so
|
|
43
|
+
plugin order cannot change it. `resolveProfile(name)` flattens one, and throws on a name the table
|
|
44
|
+
lacks. A shell
|
|
45
|
+
config without `profile` is `basic`, or `dev` for Lowdefy Dev. `app.config.ts` applies only the
|
|
46
|
+
profile's config plugins, and sets `capabilities` from it when `shell.json` has none.
|
|
47
|
+
|
|
48
|
+
The shell's version and type manifest are not in the Expo config: they are
|
|
49
|
+
`src/version/shellVersion.js` and `src/plugins/typeManifest.js`, part of the JavaScript bundle, so
|
|
50
|
+
they match the code that runs. An embedded launch reads the Expo config of the native build, so a
|
|
51
|
+
version there would keep a debug shell on Metro behind an app's `minShellVersion`, and a manifest
|
|
52
|
+
there would hide a type added since. The manifest lists every block, action and operator type the
|
|
53
|
+
shell registers (`src/plugins/types.js`, listed in
|
|
54
|
+
`src/plugins/shellPlugins.json`), with its plugin package and that package's major version.
|
|
55
|
+
`pnpm --filter=@lowdefy/shell type-manifest` rewrites it (`scripts/createTypeManifest.mjs`) after
|
|
56
|
+
a change to the plugin maps, `src/plugins/types.test.js` fails when it is stale, and
|
|
57
|
+
`pnpm release:version` rewrites it after the version bump.
|
|
58
|
+
|
|
59
|
+
## Native features
|
|
60
|
+
|
|
61
|
+
What each profile carries (`profiles.js`), with its permissions, purpose texts, privacy reasons and
|
|
62
|
+
collected data:
|
|
63
|
+
|
|
64
|
+
| Profile | Adds |
|
|
65
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
66
|
+
| `basic` | push and local notifications, haptics, share out, Photos and Files pickers (system pickers, no permission), read aloud, keep awake, icon quick actions and App Shortcuts, background fetch |
|
|
67
|
+
| `field` | the camera (photos, code and document scanning), location in use, maps (expo-maps), voice notes, screen brightness |
|
|
68
|
+
| `field-nfc` | NFC read and write (react-native-nfc-manager) |
|
|
69
|
+
| `field-tracking` | background location and geofences (expo-location, expo-task-manager) |
|
|
70
|
+
|
|
71
|
+
Lowdefy Dev (`dev`, `development`) carries `field`, `field-nfc`, `field-tracking`, `contacts` and
|
|
72
|
+
`calendar` (`includes` and `extensions` in its row), so any app's pages can be tried in it; it
|
|
73
|
+
leaves out only the widget extension.
|
|
74
|
+
|
|
75
|
+
`extensions` in `shell.json` (from `mobile.shells[].extensions`) adds opt-in rows on top of any
|
|
76
|
+
profile (`EXTENSIONS` in `profiles.js`): `share` (receiving shared items, expo-sharing's share
|
|
77
|
+
extension), `widgets` (iOS Home Screen and Lock Screen widgets, expo-widgets), `contacts` and
|
|
78
|
+
`calendar`. They need an extension target, an App Group or a heavily reviewed permission, so no
|
|
79
|
+
profile carries them. `getShellProfile` folds them in, the runtime version names them
|
|
80
|
+
(`3-field+share+widgets`), and `check:native-version` fingerprints each on `basic`.
|
|
81
|
+
|
|
82
|
+
- **Device registry.** `src/device/device.js` builds the features the shell carries
|
|
83
|
+
(`createDevice.js`: the `basic` ones in `src/device/basic`, and each profile's or extension's
|
|
84
|
+
entry module, `src/device/<field|nfc|tracking|contacts|calendar>/index.js` and
|
|
85
|
+
`src/widgets/index.js`, which a shell without it resolves to an empty module) and installs them in
|
|
86
|
+
blocks-native's registry (`installDevice`) when the bundle loads. blocks-native's native actions
|
|
87
|
+
and blocks call them; a missing feature throws naming its profile. `DeviceHosts` mounts the views
|
|
88
|
+
features open over the app (the ScanCode sheet).
|
|
89
|
+
- **App services.** `src/native/startAppServices.js` starts once the app's auth and session exist:
|
|
90
|
+
it sets the current app (`currentApp.js`) that work started outside React reaches (notification
|
|
91
|
+
buttons, background tasks), and starts notifications, the request queue, quick actions, share-in,
|
|
92
|
+
background fetch and widget refreshes. App events (`onNotification`, `onNotificationAction`,
|
|
93
|
+
`onShare`, `onShortcut`, `onGeofence`, `onRequestQueue`) run on the app context
|
|
94
|
+
(`createAppEventTrigger.js`), waiting for the first page when they come before it.
|
|
95
|
+
- **Notifications** (`src/notifications`). Categories from `mobile.notifications` are registered;
|
|
96
|
+
the token is posted to its `register` endpoint once signed in (`registerPushToken.js`) and to
|
|
97
|
+
`unregister` at sign-out, before the token is removed. A tap opens `data.path` and fires
|
|
98
|
+
`onNotificationAction`; a button with `endpointId` calls it in the background; `snooze` shows the
|
|
99
|
+
notification again later; a push with `data.refreshWidgets` refreshes the widgets.
|
|
100
|
+
- **Offline request queue** (`src/queue`). `offline: queue` requests go through the host's
|
|
101
|
+
`requestQueue` (client-core `createCallRequest`): kept in SQLite while offline, replayed in order
|
|
102
|
+
on reconnect and launch, a 2xx removes, any other answer marks failed. Sign-out clears it.
|
|
103
|
+
- **Links.** `<scheme>://shortcut/<id>` (App Shortcuts) and `<scheme>://expo-sharing` (the share
|
|
104
|
+
extension) go to their services; `https://<app url>/<path>` opens its page (universal links and
|
|
105
|
+
App Links: `appLinks.js` writes the associated domain and the Android filter).
|
|
106
|
+
- **Background.** Background fetch (`src/native/backgroundFetch.js`) and tracking
|
|
107
|
+
(`src/device/tracking`) define their tasks when the bundle loads and post to the app's endpoints
|
|
108
|
+
with the session token; tracking keeps failed posts for the next wake (`createBacklog.js`).
|
|
109
|
+
- **App Shortcuts.** `native/withAppShortcuts.js` adds `LowdefyAppShortcuts.swift` to the iOS app:
|
|
110
|
+
one App Intent whose options are the quick actions the shell set, opening
|
|
111
|
+
`<scheme>://shortcut/<id>`.
|
|
112
|
+
- **Maps.** On Android the Map block draws Google Maps, whose key comes from
|
|
113
|
+
`LOWDEFY_GOOGLE_MAPS_API_KEY` when the shell is built.
|
|
114
|
+
|
|
115
|
+
## Shell plugins
|
|
116
|
+
|
|
117
|
+
`plugins` in `shell.json` lists npm packages that add native code on top of the shell's profile:
|
|
118
|
+
`[{ name, version, typePrefix?, options? }]`, written by `lowdefy mobile config` from
|
|
119
|
+
`mobile.shells[].plugins`, which also installs them in the shell project. Each is a Lowdefy shell
|
|
120
|
+
plugin; `resolveShellPlugins.js` describes the package contract:
|
|
121
|
+
|
|
122
|
+
- `./shell` (CommonJS): a row shaped like one of `profiles.js` (`capabilities`, `modules`,
|
|
123
|
+
`plugins`, `permissions`, `privacyReasons`, `dataCollected`). `plugins` may be a function of the
|
|
124
|
+
plugin's `options`. `app.config.ts` loads it (`resolveShellPlugins.js`) and folds it into the
|
|
125
|
+
resolved profile (`applyShellPlugins.js`), so its config plugins, permissions, privacy reasons and
|
|
126
|
+
data join the profile's. Its modules are never excluded.
|
|
127
|
+
- `./native` (optional): named exports `blocks` and `actions`, `{ [typeName]: implementation }`.
|
|
128
|
+
The shell ships `src/plugins/appTypes.js` empty; `config` rewrites it in the copy to import each
|
|
129
|
+
plugin's `./native` with its `typePrefix` (`src/plugins/prefixTypes.js`), and `types.js`
|
|
130
|
+
registers them after the built-in types.
|
|
131
|
+
- `./types` (optional): `native: { blocks, actions, blockMetas, icons }`.
|
|
132
|
+
`scripts/createTypeManifest.mjs` adds `native.blocks` and `native.actions`, prefixed, to the type
|
|
133
|
+
manifest with the plugin's major version, and fails when two packages give one type name.
|
|
134
|
+
`config` runs it with `--write` in the copy, so the bundled `src/plugins/typeManifest.js` lists
|
|
135
|
+
the plugins' types.
|
|
136
|
+
|
|
137
|
+
`checkShellPlugins.js` refuses a plugin that links a module `@lowdefy/shell` carries itself
|
|
138
|
+
(`expo-camera`) or asks for a permission some profile carries and the shell's leaves out (the camera
|
|
139
|
+
on `basic`), naming the profile to pick. `config` runs it from the copied shell; `app.config.ts`
|
|
140
|
+
runs it again at prebuild. `fixtures/printer-plugin` is the test plugin.
|
|
141
|
+
|
|
142
|
+
## Store uploads
|
|
143
|
+
|
|
144
|
+
`app.config.ts` sets what App Store Connect and Google Play would otherwise ask a person on upload:
|
|
145
|
+
|
|
146
|
+
- `ios.config.usesNonExemptEncryption: false`: the shell uses only the OS's encryption.
|
|
147
|
+
- `ios.privacyManifests`: the required-reason APIs of the profile's modules (`privacyReasons` in
|
|
148
|
+
`profiles.js`, where `basic` carries React Native's and expo-sqlite's), each listed once
|
|
149
|
+
(`getPrivacyManifest.js`).
|
|
150
|
+
- From `shell.json` `icon` (a 1024×1024 PNG, relative to `shell.json`) and `color`: the app icon,
|
|
151
|
+
the `expo-splash-screen` image on `color`, and the Android adaptive icon on `color`. Its
|
|
152
|
+
foreground, the icon scaled into the centre 66% of a transparent canvas, is written beside
|
|
153
|
+
`shell.json` as `adaptive-icon-<hash>.png` (`scripts/createShellAssets.mjs`) once per icon.
|
|
154
|
+
Without `icon` (Lowdefy Dev) Expo's defaults stay.
|
|
155
|
+
|
|
156
|
+
## Versions
|
|
157
|
+
|
|
158
|
+
- `version` in `shell.json` is the store version (`major.minor.patch`), the app config's `version`.
|
|
159
|
+
Lowdefy Dev has none and builds as `0.0.0`.
|
|
160
|
+
- The shell version is the `@lowdefy/shell` package version, which the runtime sends as
|
|
161
|
+
`x-lowdefy-shell` and compares with `mobile.minShellVersion`. It is `src/version/shellVersion.js`,
|
|
162
|
+
which `pnpm --filter=@lowdefy/shell shell-version` rewrites (`scripts/createShellVersion.mjs`) and
|
|
163
|
+
`pnpm release:version` rewrites after the version bump; `src/version/shellVersion.test.js` fails
|
|
164
|
+
when it is stale.
|
|
165
|
+
- `runtimeVersion` is `<nativeVersion>-<profile>`, with `+<extension>` for each of its
|
|
166
|
+
`extensions` and `-<hash>` of the shell's `plugins` when it lists any (`runtimeVersion.js`, a
|
|
167
|
+
pure function `lowdefy mobile` reuses). Each plugin is keyed on
|
|
168
|
+
`name@version` and its `options` as JSON with keys sorted, so a new plugin version or option is a
|
|
169
|
+
new runtime version and a native build. An over-the-air update reaches only binaries with the same runtime version. Shell
|
|
170
|
+
identity is not part of it, so every shell on one `nativeVersion` and profile shares it.
|
|
171
|
+
- `nativeVersion` in `package.json` is bumped only when the shell's native dependencies or config
|
|
172
|
+
plugins change. `pnpm check:native-version` (run by the package's tests) computes Expo's
|
|
173
|
+
fingerprint of the shell's native inputs for every profile and fails when it differs from the one
|
|
174
|
+
`native-fingerprint.json` records for the current `nativeVersion`. After a bump,
|
|
175
|
+
`pnpm check:native-version --write` records the new one.
|
|
176
|
+
- With `eas`, the app config sets `owner`, `updates.url` (`https://u.expo.dev/<projectId>`) and
|
|
177
|
+
`extra.eas.projectId`.
|
|
178
|
+
|
|
179
|
+
## Boot, cache and sync
|
|
180
|
+
|
|
181
|
+
- The shell opens its one app at once. There is no app picker.
|
|
182
|
+
- The last root config and pages the app showed are read from SQLite and rendered at once. The
|
|
183
|
+
cache is keyed by app, caller (person and organisation) and roles, from the server's
|
|
184
|
+
`x-lowdefy-caller` stamp.
|
|
185
|
+
- In the background the shell fetches `GET /api/mobile/root` with `If-None-Match`, then every page
|
|
186
|
+
whose index hash changed, 4 at a time. Dynamic pages, and dev pages (no hash), are fetched on
|
|
187
|
+
each visit.
|
|
188
|
+
- Every call carries `x-lowdefy-shell` (the shell's version) and the session token as a bearer
|
|
189
|
+
token, kept in the Keychain or Keystore.
|
|
190
|
+
|
|
191
|
+
## Sign-in, unlock and sign-out
|
|
192
|
+
|
|
193
|
+
`src/auth`. The mobile root carries the app's `mobile.auth` (null for an app without auth, which
|
|
194
|
+
opens with no sign-in screen).
|
|
195
|
+
|
|
196
|
+
- With nobody signed in, the native email-code screen asks for an email, then the 6-digit code
|
|
197
|
+
(iOS offers it from Mail). It always says "If this email can sign in, a code is on its way".
|
|
198
|
+
"Other ways to sign in", `emailCode: false`, a server without email codes and a second factor
|
|
199
|
+
open the app's sign-in page in an ephemeral system sheet (`/auth/mobile`, with a state, a PKCE
|
|
200
|
+
S256 challenge and the platform). Once signed in, the page asks "Sign in to <app> on this
|
|
201
|
+
device?"; Continue creates a new session for the phone and returns a one-time token for it. iOS
|
|
202
|
+
returns to the shell's scheme; Android returns by the verified app link
|
|
203
|
+
`<app url>/auth/mobile/callback`, which `app.config.ts` declares for each app url
|
|
204
|
+
(`authCallbackIntentFilters.js`) and the app's `assetlinks.json` names the shell for. The shell
|
|
205
|
+
checks the state and exchanges the one-time token with the code verifier at
|
|
206
|
+
`/api/auth/one-time-token/verify`.
|
|
207
|
+
- The session token is kept in `expo-secure-store` on this device only: readable while unlocked,
|
|
208
|
+
behind biometrics with `unlock: biometric`, or readable after first unlock for the
|
|
209
|
+
`field-tracking` profile. It is read once per unlock and held in memory. Calls send no cookies.
|
|
210
|
+
- `unlock: biometric` shows a lock screen on launch and after `unlockAfter` seconds in the
|
|
211
|
+
background, with "Sign in again" as the way out.
|
|
212
|
+
- Sign-out, the token cleared by the core (a Logout action, a 401), and a call with the token
|
|
213
|
+
answered as the signed-out caller remove the token, every cached root and page of the app and
|
|
214
|
+
the person's storage keys, then open the app afresh. A 403 drops that caller's cache.
|
|
215
|
+
`sharedDevice: true` signs out whenever the app goes to the background.
|
|
216
|
+
|
|
217
|
+
## Version skew
|
|
218
|
+
|
|
219
|
+
- A page whose type set names a type the shell lacks, or carries at another major version, shows
|
|
220
|
+
"Update the app to open this page" with the store link. Other pages still open.
|
|
221
|
+
- `mobile.minShellVersion` above the shell's version shows the same screen for every page.
|
|
222
|
+
- A page request refused with a 409 for another build syncs the root and changed pages, then is
|
|
223
|
+
retried once when its own page did not change; otherwise the screen is rebuilt on the new page.
|
|
224
|
+
- A response answered as another caller drops the screens, syncs under the new caller's key and
|
|
225
|
+
opens home.
|
|
226
|
+
|
|
227
|
+
## Navigation
|
|
228
|
+
|
|
229
|
+
Expo Router, with one route for every page, `app/(t0,t1,t2,t3,t4)/[...path].js`, in five route
|
|
230
|
+
groups: with `mobile.menus.tabs` each tab is a group with its own native stack, under native tabs.
|
|
231
|
+
A `drawer` frame is one stack with a menu button; a `stack` frame is one stack. `Link` actions go
|
|
232
|
+
through the router; a link to a tab's page switches tab (with its query), and links to other sites
|
|
233
|
+
open in the system browser.
|
|
234
|
+
|
|
235
|
+
A `Screen` page reports its header and `src/navigation/getStackOptions.js` maps it to native stack
|
|
236
|
+
options: the title, or an iOS large title (`largeTitle`) that collapses as the page scrolls; the
|
|
237
|
+
presentation; a native search bar (`headerSearch`, focused on each show with `autoFocus`); and
|
|
238
|
+
the `headerLeft`/`headerRight` slots and `headerActions` at their natural size. On iOS each action
|
|
239
|
+
is a native bar button drawing its SF Symbol in the bar's glass, and an icon with no symbol (or
|
|
240
|
+
the slot) a custom item; elsewhere a side is a row of icon buttons. A side with nothing keeps the
|
|
241
|
+
stack's own (a drawer's menu button). Pull-to-refresh (`onRefresh`) and `onFocus` come from the
|
|
242
|
+
page's screen: `PageScreen` passes `useIsFocused()`.
|
|
243
|
+
|
|
244
|
+
Before a page is first drawn, `PageScreen` sets the bar its config gives
|
|
245
|
+
(`navigation/getInitialScreenOptions.js`: a literal `largeTitle`, `headerSearch`, `title`,
|
|
246
|
+
`presentation` and `backgroundTint`) and waits a frame: iOS keeps a large title collapsed when
|
|
247
|
+
the page's scroll view was first laid out under a standard bar, and the Screen reports its options
|
|
248
|
+
only once it renders inside that scroll view.
|
|
249
|
+
|
|
250
|
+
A Screen's `headerBottom` slot pins its blocks under the bar. `PageScreen` passes `headerInset`,
|
|
251
|
+
where the bar's bottom edge is in the page's scroll view: the native stack's
|
|
252
|
+
`useAnimatedHeaderHeight()` (from `expo-router/native-stack`), which follows a large title as it
|
|
253
|
+
collapses, when the page scrolls under the bar (`navigation/isPageUnderHeader.js`: iOS 26 and
|
|
254
|
+
later, or a large title or search bar before that), and 0 when the page sits below it (Android).
|
|
255
|
+
A page with a headerBottom hides the bar's own hairline; the strip draws one under itself.
|
|
256
|
+
A Screen with a `backgroundTint` gets a clear bar on iOS (`headerTransparent`; before iOS 26 also
|
|
257
|
+
clear at the scroll edge, with the chrome material once content scrolls under it), so the tint the
|
|
258
|
+
page draws at the screen's top shows behind the bar, and the page counts as under the bar.
|
|
259
|
+
|
|
260
|
+
Each screen keeps its page state per route instance (`useRoute().key`), so task B pushed on task A
|
|
261
|
+
has its own, and reads `_url_query` from its own route (`routePath`). The focused screen sets the
|
|
262
|
+
session's current path and instance (`setCurrentPath`), which the host's `location` and Lowdefy
|
|
263
|
+
Dev's shown page (`getShownPage`, `shownScreen` in the shell's globals) read.
|
|
264
|
+
|
|
265
|
+
Tabs (`mobile.menus.tabs`) draw their icons in the native tab bar, the selected tab in the theme's
|
|
266
|
+
`colorPrimary`. On iOS a tab draws an SF Symbol: its `sfSymbol`, or the symbol client-native's
|
|
267
|
+
`getSfSymbol` maps from its icon (`Inbox` to `tray`, `FileText` to `doc.text`, `edit` to `pencil`).
|
|
268
|
+
Elsewhere, and for an icon with no symbol, `TabIconImages` draws the Lucide icon from the item's
|
|
269
|
+
icon data once, out of sight, and reads it back as a PNG for the tab bar to tint. A tab with
|
|
270
|
+
`role: search` is iOS 26's search tab, apart at the right of the bar with a magnifier; on Android
|
|
271
|
+
and older iOS it is a tab with the search icon. Its page is a Screen with
|
|
272
|
+
`headerSearch: { autoFocus: true }`.
|
|
273
|
+
|
|
274
|
+
## Plugins, `_js`, locale and theme
|
|
275
|
+
|
|
276
|
+
- Pages run every actions-core action and the client operators of every default operator package
|
|
277
|
+
(`src/plugins/types.js`). The actions whose web bodies reach the DOM run native bodies under the
|
|
278
|
+
same names (`src/actions`): `SetFocus` focuses the control a block registered, `ScrollTo`
|
|
279
|
+
scrolls the page's ScrollView to a block's measured place, `CopyToClipboard` uses
|
|
280
|
+
`expo-clipboard`, and the local storage actions keep the app's keys in the device store, from
|
|
281
|
+
which sign-out clears the user keys. `SetDarkMode` and `SetLocale` keep their actions-core
|
|
282
|
+
bodies: they call the window members the theme provider and the shell's locale set.
|
|
283
|
+
- `_js` reaches the shell as module text: a page's `_jsEntries`, and the root's for the app
|
|
284
|
+
events. `src/js/evaluateJsEntries.js` is the one place it becomes functions, with
|
|
285
|
+
`new Function`.
|
|
286
|
+
- The app's locale is chosen as on the web: the `SetLocale` choice the device keeps, the device's
|
|
287
|
+
locale the app supports, the app's default. Every dayjs locale is bundled
|
|
288
|
+
(`src/locale/dayjsLocales.js`, written by `scripts/createDayjsLocales.mjs`) and loaded when
|
|
289
|
+
used.
|
|
290
|
+
- One theme provider holds the theme mode for every screen, from the root's `theme` and the
|
|
291
|
+
`SetDarkMode` choice; the root's `classMap`, with a dev page's own `_classMap`, styles classes.
|
|
292
|
+
- Block icons are drawn from the icon data each page carries (`_icons`), which `PageScreen` adds
|
|
293
|
+
to the icon map client-native's `components.Icon` reads (`types.icons`).
|
|
294
|
+
- Menu icons are drawn from the icon data the root carries: with client-native's `Icon` in the
|
|
295
|
+
drawer menu, and as SF Symbols or images in the tab bar (see Navigation).
|
|
296
|
+
- Hermes has no `crypto.getRandomValues`, which `_uuid` needs; `expo-crypto` fills it.
|
|
297
|
+
|
|
298
|
+
## Lowdefy Dev
|
|
299
|
+
|
|
300
|
+
`src/dev`. Lowdefy Dev is this shell built as the `dev` variant: `LOWDEFY_SHELL_VARIANT=dev | release`
|
|
301
|
+
(`shellVariant.js`), `dev` by default for Lowdefy Dev (no `LOWDEFY_SHELL_CONFIG`) and `release` for
|
|
302
|
+
every other shell. A release bundle resolves `src/dev/index.js` to an empty module
|
|
303
|
+
(`metro.config.js`), so a customer shell carries none of it. Every shell
|
|
304
|
+
leaves out the native modules its profile does not carry, and a release shell also leaves out
|
|
305
|
+
Lowdefy Dev's own (`expo-sensors`, `react-native-view-shot`, `expo-dev-client`) whatever its profile
|
|
306
|
+
(`getExclusions` in `shellVariant.js`): a `basic` shell links no `expo-camera`. `app.config.ts`
|
|
307
|
+
leaves out their config plugins and blocks their Android permissions, `withExcludedNativeModules.js`
|
|
308
|
+
passes them as exclusions to Expo autolinking in the generated Podfile and settings.gradle, and
|
|
309
|
+
`metro.config.js` resolves the left-out profiles' JavaScript entries to empty modules.
|
|
310
|
+
|
|
311
|
+
`pnpm check:release-bundle` checks a release shell (Lowdefy Dev's, or the one
|
|
312
|
+
`LOWDEFY_SHELL_CONFIG` names) on each release profile (`--profile` names one) and both platforms:
|
|
313
|
+
its bundle holds neither the dev-tab client's marker string nor a left-out module's native name,
|
|
314
|
+
and an `expo prebuild` of it links none of the left-out modules and asks for none of their
|
|
315
|
+
permissions. `--control` first checks that the `dev` variant holds and links them.
|
|
316
|
+
|
|
317
|
+
- **Connect.** `lowdefy-dev://connect?url=<server>&pair=<token>` (the QR code `lowdefy dev --lan`
|
|
318
|
+
prints, or the link `--ios`/`--android` open), a scanned code (`expo-camera`), a typed address, or
|
|
319
|
+
one of the last 5 servers. A server not on this device is confirmed first ("Connect to
|
|
320
|
+
`<host>`?"); a simulator's or USB phone's localhost connects at once. Lowdefy Dev opens one dev
|
|
321
|
+
server at a time; switching server closes the open app. Each server is its own app, with its own
|
|
322
|
+
cache and session, and every call to it (fetch and websocket) carries its pairing
|
|
323
|
+
token as `x-lowdefy-pair`.
|
|
324
|
+
- **Shake menu** (or `lowdefy-dev://menu`): reload, switch server, clear data, show logs, the block
|
|
325
|
+
gallery, and the shell version.
|
|
326
|
+
- **Dev-tab client.** The open app joins the dev server's tab channel as a browser tab does: it
|
|
327
|
+
registers on `/api/reload` (an event stream read from `expo/fetch`, or polls of
|
|
328
|
+
`/api/reload?transport=poll` every second when the stream cannot be read), posts the page it shows
|
|
329
|
+
to `/api/dev-inspect/page`, and answers on `/api/dev-inspect`:
|
|
330
|
+
- `reload`: refetches the root and the page on screen and rebuilds the screens with empty page
|
|
331
|
+
state, staying on the page.
|
|
332
|
+
- `inspect-request`, `eval-request`: the browser tab's payloads (shared fixtures with the dev
|
|
333
|
+
server's tests).
|
|
334
|
+
- `step-request` `{ step }`: a journey step by block id (`click`, `fill`, `select`,
|
|
335
|
+
`press: 'Enter'`, `wait`, `screenshot`), run through the engine; it waits for the action chain
|
|
336
|
+
to settle. A screenshot is the root view as a base64 PNG (`react-native-view-shot`).
|
|
337
|
+
- `inject` `{ inject: { kind, value, name } }`: fakes the next device result
|
|
338
|
+
(`src/dev/inject`): `scan` `{ data, type }`, `photo` `{ base64, ... }` and `nfc` for the next
|
|
339
|
+
read; `transcript` `{ partials?, final }` or `{ error }` for the next dictation session, which
|
|
340
|
+
plays the partials and the final a moment apart, while a session with nothing injected uses
|
|
341
|
+
the microphone (blocks-native's `installDictationSource`, which Lowdefy Dev fills with
|
|
342
|
+
`injected`; `permission` `microphone` answers its prompt);
|
|
343
|
+
`location` `{ latitude, longitude, ... }`, or a route as a list, read one at a time with
|
|
344
|
+
the last one staying; `permission` with `name` and `'granted' | 'denied'`, standing until
|
|
345
|
+
changed; `network` `'offline' | 'online'`, which fails the app's every fetch as offline (the tab
|
|
346
|
+
client keeps its own) and takes the shell's online state offline (`src/host/networkStatus.js`,
|
|
347
|
+
which expo-network also feeds), so the NoConnection banner shows and the app's `offline` events
|
|
348
|
+
fire; `push` `{ title, body, data, categoryId?, open?, actionId?, text? }`, shown now and,
|
|
349
|
+
with `open` or `actionId`, tapped or answered (`injectPush.js`); `share` `{ text?, url?,
|
|
350
|
+
files? }`, as if shared into the app (`injectShare.js`); `clear`.
|
|
351
|
+
Device actions read these with `injected(name)` (`src/device/injected.js`), which returns
|
|
352
|
+
nothing in a release shell; `GeolocationCurrentPosition` answers with an injected location.
|
|
353
|
+
- **Recorder** (`src/dev/recorder`). What the developer does is recorded as the web recorder records
|
|
354
|
+
it: the engine trace's events paired with the presses and changes client-native reports by block
|
|
355
|
+
id, a pageview when the page on screen changes, password inputs redacted, each record labelled
|
|
356
|
+
`device: ios | android` and `shell_version`, posted to `/api/dev-recording`. `lowdefy journeys
|
|
357
|
+
session` lists and prints phone sessions as it does browser ones.
|
|
358
|
+
|
|
359
|
+
## Checks
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
pnpm --filter=@lowdefy/shell test # Jest, and the hello fixture build
|
|
363
|
+
pnpm --filter=@lowdefy/shell export:ios # Metro and Hermes bundle, no simulator needed
|
|
364
|
+
pnpm --filter=@lowdefy/shell export:android
|
|
365
|
+
pnpm --filter=@lowdefy/shell check:release-bundle # each release profile links only its modules
|
|
366
|
+
pnpm --filter=@lowdefy/shell check:native-version # native code changed only with a nativeVersion bump
|
|
367
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
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 { useLocalSearchParams } from 'expo-router';
|
|
19
|
+
import { serializeQuery } from '@lowdefy/client-core';
|
|
20
|
+
|
|
21
|
+
import getTabHref from '../../src/navigation/getTabHref.js';
|
|
22
|
+
import PageScreen from '../../src/components/PageScreen.js';
|
|
23
|
+
import SwitchToTab from '../../src/components/SwitchToTab.js';
|
|
24
|
+
import { useSessionState } from '../../src/ShellContext.js';
|
|
25
|
+
|
|
26
|
+
// Every Lowdefy page, by path. The page and its path params come from the page index; the route's
|
|
27
|
+
// other params are the page's query. A tab's own page is shown on its tab, not stacked here.
|
|
28
|
+
export default function PathRoute() {
|
|
29
|
+
const { path, ...query } = useLocalSearchParams();
|
|
30
|
+
const { root } = useSessionState();
|
|
31
|
+
const segments = Array.isArray(path) ? path : [path];
|
|
32
|
+
const pagePath = segments.join('/');
|
|
33
|
+
const tabHref = getTabHref({ path: pagePath, query: serializeQuery(query), root });
|
|
34
|
+
if (tabHref !== null) {
|
|
35
|
+
return <SwitchToTab href={tabHref} />;
|
|
36
|
+
}
|
|
37
|
+
return <PageScreen path={pagePath} query={serializeQuery(query)} />;
|
|
38
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { Stack } from 'expo-router';
|
|
19
|
+
|
|
20
|
+
import getFrame from '../../src/navigation/getFrame.js';
|
|
21
|
+
import { getSlotIndex } from '../../src/navigation/frameSlots.js';
|
|
22
|
+
import MenuButton from '../../src/components/MenuButton.js';
|
|
23
|
+
import SlotContext from '../../src/navigation/SlotContext.js';
|
|
24
|
+
import { useSessionState } from '../../src/ShellContext.js';
|
|
25
|
+
|
|
26
|
+
// A deep link opens on top of the group's first screen, so back leads to it.
|
|
27
|
+
export const unstable_settings = { initialRouteName: 'index' };
|
|
28
|
+
|
|
29
|
+
// The stack of one frame group (a tab, or the app's one stack). A drawer frame puts its menu
|
|
30
|
+
// button in the header of the group's first screen. Expo Router passes the group's own segment,
|
|
31
|
+
// such as `(t1)`, to a layout shared by several groups; its screens read the group from SlotContext.
|
|
32
|
+
export default function GroupLayout({ segment }) {
|
|
33
|
+
const { root } = useSessionState();
|
|
34
|
+
const frame = getFrame(root.menus);
|
|
35
|
+
return (
|
|
36
|
+
<SlotContext.Provider value={getSlotIndex(segment)}>
|
|
37
|
+
<Stack screenOptions={{ title: '' }}>
|
|
38
|
+
<Stack.Screen
|
|
39
|
+
name="index"
|
|
40
|
+
options={
|
|
41
|
+
frame.kind === 'drawer' ? { headerLeft: () => <MenuButton items={frame.items} /> } : {}
|
|
42
|
+
}
|
|
43
|
+
/>
|
|
44
|
+
<Stack.Screen name="[...path]" />
|
|
45
|
+
</Stack>
|
|
46
|
+
</SlotContext.Provider>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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, { useContext } from 'react';
|
|
18
|
+
import { useLocalSearchParams } from 'expo-router';
|
|
19
|
+
import { serializeQuery } from '@lowdefy/client-core';
|
|
20
|
+
|
|
21
|
+
import getSlotRootPath from '../../src/navigation/getSlotRootPath.js';
|
|
22
|
+
import MessageScreen from '../../src/components/MessageScreen.js';
|
|
23
|
+
import PageScreen from '../../src/components/PageScreen.js';
|
|
24
|
+
import SlotContext from '../../src/navigation/SlotContext.js';
|
|
25
|
+
import { useSessionState } from '../../src/ShellContext.js';
|
|
26
|
+
|
|
27
|
+
// The first screen of a frame group: its tab's page, or the app's home page. A link to a tab's
|
|
28
|
+
// page switches to the tab (toRouterHref) with its query as the route's params.
|
|
29
|
+
export default function GroupIndex() {
|
|
30
|
+
const slotIndex = useContext(SlotContext);
|
|
31
|
+
const query = useLocalSearchParams();
|
|
32
|
+
const { root } = useSessionState();
|
|
33
|
+
const path = getSlotRootPath({ root, slotIndex });
|
|
34
|
+
if (path === null) {
|
|
35
|
+
return <MessageScreen testID="shell-no-home" title="This app has no page to open" />;
|
|
36
|
+
}
|
|
37
|
+
return <PageScreen path={path} query={serializeQuery(query)} />;
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
// Links that are not page paths:
|
|
18
|
+
// - The sign-in sheet's Android app link (/auth/mobile/callback) carries a one-time token that
|
|
19
|
+
// signInWithSheet reads from the sheet's result; the router leaves it alone.
|
|
20
|
+
// - A Lowdefy Dev link (lowdefy-dev://connect, lowdefy-dev://menu): Lowdefy Dev handles it
|
|
21
|
+
// (src/dev). A release shell has no developer features and routes every other link.
|
|
22
|
+
// - A shortcut link (<scheme>://shortcut/<id>, from Siri and Spotlight) and the share extension's
|
|
23
|
+
// link (<scheme>://expo-sharing): the shell's shortcut and share-in services handle them
|
|
24
|
+
// (src/native).
|
|
25
|
+
// - A link to the app's own site (a universal link or Android app link): opened as its page path.
|
|
26
|
+
import { redirectDevLink } from '../src/dev/index.js';
|
|
27
|
+
import getShellConfig from '../src/config/getShellConfig.js';
|
|
28
|
+
import isSheetReturnLink from '../src/auth/isSheetReturnLink.js';
|
|
29
|
+
import parseShellLink from '../src/native/parseShellLink.js';
|
|
30
|
+
import toAppLinkPath from '../src/navigation/toAppLinkPath.js';
|
|
31
|
+
|
|
32
|
+
export function redirectSystemPath({ initial, path }) {
|
|
33
|
+
if (isSheetReturnLink(path) || parseShellLink(path) !== null) {
|
|
34
|
+
return initial ? '/' : null;
|
|
35
|
+
}
|
|
36
|
+
const { app, environment } = getShellConfig();
|
|
37
|
+
const pagePath = toAppLinkPath({ appUrl: app.urls[environment], path });
|
|
38
|
+
return redirectDevLink ? redirectDevLink({ initial, path: pagePath }) : pagePath;
|
|
39
|
+
}
|
package/app/_layout.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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 * as SplashScreen from 'expo-splash-screen';
|
|
19
|
+
|
|
20
|
+
import installGetRandomValues from '../src/polyfills/installGetRandomValues.js';
|
|
21
|
+
import ShellRoot from '../src/components/ShellRoot.js';
|
|
22
|
+
// Lowdefy Dev's developer features; undefined in a release shell, whose bundle leaves them out.
|
|
23
|
+
import { DevShell } from '../src/dev/index.js';
|
|
24
|
+
|
|
25
|
+
installGetRandomValues();
|
|
26
|
+
|
|
27
|
+
// The launch screen stays until the first screen is ready: the app's home page from the cache (or
|
|
28
|
+
// from the server on a first launch).
|
|
29
|
+
SplashScreen.preventAutoHideAsync();
|
|
30
|
+
|
|
31
|
+
// A render fault outside a block is reported and shown with a retry, not a closed app.
|
|
32
|
+
export { default as ErrorBoundary } from '../src/components/RouteErrorScreen.js';
|
|
33
|
+
|
|
34
|
+
export default function RootLayout() {
|
|
35
|
+
return DevShell ? <DevShell /> : <ShellRoot />;
|
|
36
|
+
}
|