@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,93 @@
|
|
|
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 createSqliteCacheStore from './createSqliteCacheStore.js';
|
|
20
|
+
|
|
21
|
+
// A stand-in for an expo-sqlite database over the two tables the store uses.
|
|
22
|
+
function createFakeDb() {
|
|
23
|
+
const roots = new Map();
|
|
24
|
+
const pages = new Map();
|
|
25
|
+
return {
|
|
26
|
+
execAsync: jest.fn(async () => undefined),
|
|
27
|
+
getFirstAsync: jest.fn(async (sql, [cacheKey]) => roots.get(cacheKey) ?? null),
|
|
28
|
+
getAllAsync: jest.fn(async (sql, [cacheKey]) =>
|
|
29
|
+
[...pages.values()].filter((row) => row.cache_key === cacheKey)
|
|
30
|
+
),
|
|
31
|
+
runAsync: jest.fn(async (sql, params) => {
|
|
32
|
+
if (sql.startsWith('INSERT OR REPLACE INTO roots')) {
|
|
33
|
+
const [cacheKey, root, rootEtag, user] = params;
|
|
34
|
+
roots.set(cacheKey, { root, root_etag: rootEtag, user });
|
|
35
|
+
} else if (sql.startsWith('INSERT OR REPLACE INTO pages')) {
|
|
36
|
+
const [cacheKey, pageId, page] = params;
|
|
37
|
+
pages.set(`${cacheKey}|${pageId}`, { cache_key: cacheKey, page_id: pageId, page });
|
|
38
|
+
} else if (sql.startsWith('DELETE FROM roots WHERE cache_key')) {
|
|
39
|
+
roots.delete(params[0]);
|
|
40
|
+
} else if (sql.startsWith('DELETE FROM pages WHERE cache_key = ? AND')) {
|
|
41
|
+
pages.delete(`${params[0]}|${params[1]}`);
|
|
42
|
+
} else if (sql.startsWith('DELETE FROM pages WHERE cache_key')) {
|
|
43
|
+
[...pages.keys()]
|
|
44
|
+
.filter((key) => pages.get(key).cache_key === params[0])
|
|
45
|
+
.forEach((key) => pages.delete(key));
|
|
46
|
+
} else if (sql.startsWith('DELETE FROM roots WHERE substr')) {
|
|
47
|
+
[...roots.keys()]
|
|
48
|
+
.filter((key) => key.startsWith(params[1]))
|
|
49
|
+
.forEach((key) => roots.delete(key));
|
|
50
|
+
} else if (sql.startsWith('DELETE FROM pages WHERE substr')) {
|
|
51
|
+
[...pages.keys()]
|
|
52
|
+
.filter((key) => pages.get(key).cache_key.startsWith(params[1]))
|
|
53
|
+
.forEach((key) => pages.delete(key));
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test('the SQLite cache keeps a root and its pages per cache key', async () => {
|
|
60
|
+
const db = createFakeDb();
|
|
61
|
+
const store = createSqliteCacheStore({ db });
|
|
62
|
+
expect(await store.read('field:c1:r1')).toBeNull();
|
|
63
|
+
await store.writeRoot({
|
|
64
|
+
cacheKey: 'field:c1:r1',
|
|
65
|
+
entry: { root: { homePageId: 'today' }, rootEtag: '"r1"', user: { id: 'u1' } },
|
|
66
|
+
});
|
|
67
|
+
await store.writePage({ cacheKey: 'field:c1:r1', pageId: 'today', page: { hash: 'h1' } });
|
|
68
|
+
await store.writePage({ cacheKey: 'field:c1:r1', pageId: 'jobs', page: { hash: 'h2' } });
|
|
69
|
+
await store.writePage({ cacheKey: 'field:c2:r1', pageId: 'today', page: { hash: 'other' } });
|
|
70
|
+
await store.removePages({ cacheKey: 'field:c1:r1', pageIds: ['jobs'] });
|
|
71
|
+
expect(await store.read('field:c1:r1')).toEqual({
|
|
72
|
+
root: { homePageId: 'today' },
|
|
73
|
+
rootEtag: '"r1"',
|
|
74
|
+
user: { id: 'u1' },
|
|
75
|
+
pages: { today: { hash: 'h1' } },
|
|
76
|
+
});
|
|
77
|
+
expect(db.execAsync).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('the SQLite cache drops one caller, or every caller of one app', async () => {
|
|
81
|
+
const store = createSqliteCacheStore({ db: createFakeDb() });
|
|
82
|
+
const entry = { root: {}, rootEtag: null, user: null };
|
|
83
|
+
await store.writeRoot({ cacheKey: 'field:c1:r1', entry });
|
|
84
|
+
await store.writePage({ cacheKey: 'field:c1:r1', pageId: 'today', page: { hash: 'h1' } });
|
|
85
|
+
await store.writeRoot({ cacheKey: 'field:c2:r1', entry });
|
|
86
|
+
await store.writeRoot({ cacheKey: 'fieldwork:c1:r1', entry });
|
|
87
|
+
await store.removeEntry({ cacheKey: 'field:c2:r1' });
|
|
88
|
+
expect(await store.read('field:c2:r1')).toBeNull();
|
|
89
|
+
expect(await store.read('field:c1:r1')).not.toBeNull();
|
|
90
|
+
await store.removeApp({ appId: 'field' });
|
|
91
|
+
expect(await store.read('field:c1:r1')).toBeNull();
|
|
92
|
+
expect(await store.read('fieldwork:c1:r1')).not.toBeNull();
|
|
93
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
// The cache a shell keeps for one app and caller. The server filters an app's pages, menus and
|
|
18
|
+
// page index by the caller's roles, so pages are kept per app, per person and organisation (the
|
|
19
|
+
// caller hash of the server's x-lowdefy-caller stamp) and per set of roles (its roles hash).
|
|
20
|
+
// stamp is the parsed stamp of the root response; a response without one is the anonymous caller.
|
|
21
|
+
function getCacheKey({ appId, stamp }) {
|
|
22
|
+
return [appId, stamp?.caller ?? '', stamp?.roles ?? ''].join(':');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default getCacheKey;
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
// The device key holding the cache key an app last opened with, read on start to render at once.
|
|
18
|
+
function getLastCacheKeyName(appId) {
|
|
19
|
+
return `lowdefy-shell:last-cache-key:${appId}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default getLastCacheKeyName;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React, { useEffect, useState } from 'react';
|
|
18
|
+
import { View } from 'react-native';
|
|
19
|
+
import * as SplashScreen from 'expo-splash-screen';
|
|
20
|
+
|
|
21
|
+
import AuthGate from '../auth/AuthGate.js';
|
|
22
|
+
import createShellApp from '../createShellApp.js';
|
|
23
|
+
import DeviceHosts from '../device/DeviceHosts.js';
|
|
24
|
+
import LoadingScreen from './LoadingScreen.js';
|
|
25
|
+
import MessageScreen from './MessageScreen.js';
|
|
26
|
+
import NoConnectionBanner from './NoConnectionBanner.js';
|
|
27
|
+
import reportShellError from '../host/reportShellError.js';
|
|
28
|
+
import ShellContext, { useSessionState } from '../ShellContext.js';
|
|
29
|
+
import ShellThemeProvider from './ShellThemeProvider.js';
|
|
30
|
+
import startAppServices from '../native/startAppServices.js';
|
|
31
|
+
|
|
32
|
+
function AppGate({ children }) {
|
|
33
|
+
const { status, error } = useSessionState();
|
|
34
|
+
const value = React.useContext(ShellContext);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (status !== 'loading') SplashScreen.hideAsync();
|
|
37
|
+
}, [status]);
|
|
38
|
+
if (status === 'loading') {
|
|
39
|
+
return <LoadingScreen />;
|
|
40
|
+
}
|
|
41
|
+
if (status === 'error') {
|
|
42
|
+
return (
|
|
43
|
+
<MessageScreen
|
|
44
|
+
action={{ title: 'Try again', onPress: () => value.session.start() }}
|
|
45
|
+
body={error?.message}
|
|
46
|
+
testID="shell-app-error"
|
|
47
|
+
title="This app could not be opened"
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return (
|
|
52
|
+
<View style={{ flex: 1 }}>
|
|
53
|
+
<NoConnectionBanner />
|
|
54
|
+
<ShellThemeProvider>{children}</ShellThemeProvider>
|
|
55
|
+
</View>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Opens the shell's app (shell.app): builds its auth, session and host, renders its last pages from
|
|
60
|
+
// the cache and syncs it with the app's server. A sign-out opens it afresh, with nothing of the
|
|
61
|
+
// person's kept.
|
|
62
|
+
function AppProvider({ children, shell }) {
|
|
63
|
+
const [value, setValue] = useState(null);
|
|
64
|
+
const [openError, setOpenError] = useState(null);
|
|
65
|
+
const [openings, setOpenings] = useState(0);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
let active = true;
|
|
68
|
+
let opened = null;
|
|
69
|
+
let services = null;
|
|
70
|
+
setValue(null);
|
|
71
|
+
setOpenError(null);
|
|
72
|
+
const onSignedOut = () => setOpenings((count) => count + 1);
|
|
73
|
+
createShellApp({ onSignedOut, shell })
|
|
74
|
+
.then((created) => {
|
|
75
|
+
if (!active) return;
|
|
76
|
+
const { auth, host, locale, session } = created;
|
|
77
|
+
opened = auth;
|
|
78
|
+
services = startAppServices({ app: { ...created, shell } });
|
|
79
|
+
setValue({ auth, host, locale, services, session, shell });
|
|
80
|
+
auth.start({ session });
|
|
81
|
+
})
|
|
82
|
+
.catch((error) => {
|
|
83
|
+
// Without this the app would wait on the loading screen for ever.
|
|
84
|
+
if (!active) return;
|
|
85
|
+
reportShellError({ error });
|
|
86
|
+
setOpenError(error);
|
|
87
|
+
SplashScreen.hideAsync();
|
|
88
|
+
});
|
|
89
|
+
return () => {
|
|
90
|
+
active = false;
|
|
91
|
+
services?.stop();
|
|
92
|
+
opened?.dispose();
|
|
93
|
+
};
|
|
94
|
+
}, [openings, shell]);
|
|
95
|
+
if (openError !== null) {
|
|
96
|
+
return (
|
|
97
|
+
<MessageScreen
|
|
98
|
+
action={{ title: 'Try again', onPress: () => setOpenings((count) => count + 1) }}
|
|
99
|
+
body={openError.message}
|
|
100
|
+
testID="shell-open-error"
|
|
101
|
+
title="This app could not be opened"
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (value === null) {
|
|
106
|
+
return <LoadingScreen />;
|
|
107
|
+
}
|
|
108
|
+
return (
|
|
109
|
+
<ShellContext.Provider value={value}>
|
|
110
|
+
<AuthGate>
|
|
111
|
+
<AppGate>{children}</AppGate>
|
|
112
|
+
</AuthGate>
|
|
113
|
+
<DeviceHosts />
|
|
114
|
+
</ShellContext.Provider>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default AppProvider;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
18
|
+
import { Platform } from 'react-native';
|
|
19
|
+
import { router, Stack } from 'expo-router';
|
|
20
|
+
import { NativeTabs } from 'expo-router/native-tabs';
|
|
21
|
+
import { useTheme } from '@lowdefy/client-native';
|
|
22
|
+
|
|
23
|
+
import FRAME_SLOTS from '../navigation/frameSlots.js';
|
|
24
|
+
import getFrame from '../navigation/getFrame.js';
|
|
25
|
+
import getTabIcons from '../navigation/getTabIcons.js';
|
|
26
|
+
import TabIconImages from './TabIconImages.js';
|
|
27
|
+
import { useSessionState, useShell } from '../ShellContext.js';
|
|
28
|
+
|
|
29
|
+
// One tab's icon: its SF Symbol on iOS, else its Lucide icon drawn as an image once ready. It is
|
|
30
|
+
// called, not rendered as a component: Expo Router reads a trigger's options from its direct
|
|
31
|
+
// NativeTabs.Trigger.Icon and Label children only, and drops any other element, even one that
|
|
32
|
+
// renders an Icon.
|
|
33
|
+
function renderTabIcon({ icon, image }) {
|
|
34
|
+
if (icon.sf !== null) {
|
|
35
|
+
return <NativeTabs.Trigger.Icon sf={icon.sf} />;
|
|
36
|
+
}
|
|
37
|
+
if (image === undefined) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return <NativeTabs.Trigger.Icon renderingMode="template" src={image} />;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The tabs frame: a native tab bar, each tab a route group with its own stack. The selected tab
|
|
44
|
+
// takes the theme's tint. iOS 26 draws the bar's glass itself; before it, the bar blurs in the
|
|
45
|
+
// mode showing, and Android's bar takes the cell colour. A `role: search` item is iOS 26's search tab, set apart at the
|
|
46
|
+
// right of the bar with a magnifier; elsewhere it is a tab with its search icon.
|
|
47
|
+
function TabsFrame({ items }) {
|
|
48
|
+
const { isDark, tokens } = useTheme();
|
|
49
|
+
const icons = useMemo(() => getTabIcons({ items, platform: Platform.OS }), [items]);
|
|
50
|
+
const [images, setImages] = useState({});
|
|
51
|
+
const onImage = useCallback(
|
|
52
|
+
(pageId, source) => setImages((current) => ({ ...current, [pageId]: source })),
|
|
53
|
+
[]
|
|
54
|
+
);
|
|
55
|
+
const imageItems = icons.filter((icon) => icon.iconData !== null);
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<NativeTabs
|
|
59
|
+
backgroundColor={Platform.OS === 'ios' ? undefined : tokens.colorCell}
|
|
60
|
+
blurEffect={isDark ? 'systemChromeMaterialDark' : 'systemChromeMaterialLight'}
|
|
61
|
+
iconColor={{ selected: tokens.colorTint }}
|
|
62
|
+
labelStyle={{ selected: { color: tokens.colorTint } }}
|
|
63
|
+
tintColor={tokens.colorTint}
|
|
64
|
+
>
|
|
65
|
+
{FRAME_SLOTS.map((slot, index) => {
|
|
66
|
+
const item = items[index];
|
|
67
|
+
return (
|
|
68
|
+
<NativeTabs.Trigger
|
|
69
|
+
hidden={item === undefined}
|
|
70
|
+
key={slot}
|
|
71
|
+
name={`(${slot})`}
|
|
72
|
+
role={item?.role}
|
|
73
|
+
>
|
|
74
|
+
{item === undefined
|
|
75
|
+
? null
|
|
76
|
+
: renderTabIcon({ icon: icons[index], image: images[item.pageId] })}
|
|
77
|
+
<NativeTabs.Trigger.Label>
|
|
78
|
+
{item?.title ?? item?.pageId ?? ''}
|
|
79
|
+
</NativeTabs.Trigger.Label>
|
|
80
|
+
</NativeTabs.Trigger>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
</NativeTabs>
|
|
84
|
+
{imageItems.length > 0 ? <TabIconImages items={imageItems} onImage={onImage} /> : null}
|
|
85
|
+
</>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The app's frame from its mobile menus: native tabs, one stack per tab, or one stack (with a menu
|
|
90
|
+
// button for a drawer). Each tab is a route group of app/(t0,t1,t2,t3,t4); tabs past the menu's
|
|
91
|
+
// items are hidden.
|
|
92
|
+
function Frame() {
|
|
93
|
+
const { session } = useShell();
|
|
94
|
+
const state = useSessionState();
|
|
95
|
+
const frame = getFrame(state.root.menus);
|
|
96
|
+
|
|
97
|
+
// After a caller change the shell starts again from the home page.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!session.takeHomeRequest()) return;
|
|
100
|
+
if (router.canDismiss()) router.dismissAll();
|
|
101
|
+
router.replace('/');
|
|
102
|
+
}, [session, state.homeRequests]);
|
|
103
|
+
|
|
104
|
+
if (frame.kind === 'tabs') {
|
|
105
|
+
return <TabsFrame items={frame.items} />;
|
|
106
|
+
}
|
|
107
|
+
return (
|
|
108
|
+
<Stack initialRouteName="(t0)" screenOptions={{ headerShown: false }}>
|
|
109
|
+
{FRAME_SLOTS.map((slot) => (
|
|
110
|
+
<Stack.Screen key={slot} name={`(${slot})`} />
|
|
111
|
+
))}
|
|
112
|
+
</Stack>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default Frame;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { jest } from '@jest/globals';
|
|
19
|
+
import { render } from '@testing-library/react-native';
|
|
20
|
+
import { ThemeContext } from '@lowdefy/client-native';
|
|
21
|
+
|
|
22
|
+
import Frame from './Frame.js';
|
|
23
|
+
|
|
24
|
+
// The native tabs as plain elements that keep their props, so a test reads the tab bar. A trigger
|
|
25
|
+
// keeps only its direct Icon and Label children, as Expo Router's convertTabPropsToOptions reads a
|
|
26
|
+
// trigger's options: any other element, even one that renders an Icon, is dropped.
|
|
27
|
+
jest.mock('expo-router/native-tabs', () => {
|
|
28
|
+
const { Children, createElement, isValidElement } = jest.requireActual('react');
|
|
29
|
+
const element = (name) => (props) => createElement(name, props, props.children);
|
|
30
|
+
const NativeTabs = element('NativeTabs');
|
|
31
|
+
const Icon = element('TriggerIcon');
|
|
32
|
+
const Label = ({ children }) => createElement('TriggerLabel', { label: children });
|
|
33
|
+
NativeTabs.Trigger = (props) =>
|
|
34
|
+
createElement(
|
|
35
|
+
'Trigger',
|
|
36
|
+
props,
|
|
37
|
+
Children.toArray(props.children).filter(
|
|
38
|
+
(child) => isValidElement(child) && (child.type === Icon || child.type === Label)
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
NativeTabs.Trigger.Icon = Icon;
|
|
42
|
+
NativeTabs.Trigger.Label = Label;
|
|
43
|
+
return { NativeTabs };
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
jest.mock('expo-router', () => ({ router: {}, Stack: () => null }));
|
|
47
|
+
|
|
48
|
+
const mockState = {};
|
|
49
|
+
jest.mock('../ShellContext.js', () => ({
|
|
50
|
+
useSessionState: () => mockState,
|
|
51
|
+
useShell: () => ({ session: { takeHomeRequest: () => false } }),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
function findAll(node, predicate, found = []) {
|
|
55
|
+
if (node === null || typeof node === 'string') return found;
|
|
56
|
+
if (predicate(node)) found.push(node);
|
|
57
|
+
(node.children ?? []).forEach((child) => findAll(child, predicate, found));
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const inbox = { node: [['path', { d: 'M1 1' }]] };
|
|
62
|
+
const tokens = { colorCell: '#FFFFFF', colorLabel: '#000000', colorTint: '#0A72D8' };
|
|
63
|
+
|
|
64
|
+
test('native tab bar items take SF Symbols, the search role and the theme tint', async () => {
|
|
65
|
+
mockState.homeRequests = 0;
|
|
66
|
+
mockState.root = {
|
|
67
|
+
menus: {
|
|
68
|
+
tabs: [
|
|
69
|
+
{ pageId: 'my-work', title: 'My work', icon: 'Inbox', iconData: inbox },
|
|
70
|
+
{ pageId: 'tasks', title: 'Tasks', icon: 'SquareCheck', sfSymbol: 'checklist' },
|
|
71
|
+
{ pageId: 'search', title: 'Search', icon: 'search', role: 'search' },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const view = await render(
|
|
76
|
+
<ThemeContext.Provider value={{ isDark: false, preference: 'system', tokens }}>
|
|
77
|
+
<Frame />
|
|
78
|
+
</ThemeContext.Provider>
|
|
79
|
+
);
|
|
80
|
+
const tabBar = findAll(view.root, (node) => node.type === 'NativeTabs')[0];
|
|
81
|
+
expect(tabBar.props.tintColor).toBe('#0A72D8');
|
|
82
|
+
expect(tabBar.props.iconColor).toEqual({ selected: '#0A72D8' });
|
|
83
|
+
expect(tabBar.props.blurEffect).toBe('systemChromeMaterialLight');
|
|
84
|
+
const triggers = findAll(view.root, (node) => node.type === 'Trigger');
|
|
85
|
+
expect(triggers.map((trigger) => [trigger.props.name, trigger.props.hidden])).toEqual([
|
|
86
|
+
['(t0)', false],
|
|
87
|
+
['(t1)', false],
|
|
88
|
+
['(t2)', false],
|
|
89
|
+
['(t3)', true],
|
|
90
|
+
['(t4)', true],
|
|
91
|
+
]);
|
|
92
|
+
expect(triggers[2].props.role).toBe('search');
|
|
93
|
+
const labels = findAll(view.root, (node) => node.type === 'TriggerLabel');
|
|
94
|
+
expect(labels.slice(0, 3).map((label) => label.props.label)).toEqual([
|
|
95
|
+
'My work',
|
|
96
|
+
'Tasks',
|
|
97
|
+
'Search',
|
|
98
|
+
]);
|
|
99
|
+
const icons = findAll(view.root, (node) => node.type === 'TriggerIcon');
|
|
100
|
+
expect(icons.map((icon) => icon.props.sf)).toEqual(['tray', 'checklist', 'magnifyingglass']);
|
|
101
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
19
|
+
|
|
20
|
+
import styles from './styles.js';
|
|
21
|
+
import useSurfaceTokens from './useSurfaceTokens.js';
|
|
22
|
+
|
|
23
|
+
function LoadingScreen() {
|
|
24
|
+
const tokens = useSurfaceTokens();
|
|
25
|
+
return (
|
|
26
|
+
<View
|
|
27
|
+
style={[styles.centered, { backgroundColor: tokens.colorGroupedBackground }]}
|
|
28
|
+
testID="shell-loading"
|
|
29
|
+
>
|
|
30
|
+
<ActivityIndicator color={tokens.colorSecondaryLabel} />
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default LoadingScreen;
|
|
@@ -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 } from 'react';
|
|
18
|
+
import { Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
19
|
+
import { router } from 'expo-router';
|
|
20
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
21
|
+
import { Icon } from '@lowdefy/client-native';
|
|
22
|
+
|
|
23
|
+
import { useSessionState } from '../ShellContext.js';
|
|
24
|
+
import useSurfaceTokens from './useSurfaceTokens.js';
|
|
25
|
+
|
|
26
|
+
const styles = StyleSheet.create({
|
|
27
|
+
button: {
|
|
28
|
+
paddingHorizontal: 8,
|
|
29
|
+
paddingVertical: 4,
|
|
30
|
+
},
|
|
31
|
+
buttonText: {
|
|
32
|
+
fontSize: 16,
|
|
33
|
+
},
|
|
34
|
+
backdrop: {
|
|
35
|
+
flex: 1,
|
|
36
|
+
flexDirection: 'row',
|
|
37
|
+
backgroundColor: 'rgba(0,0,0,0.3)',
|
|
38
|
+
},
|
|
39
|
+
sheet: {
|
|
40
|
+
width: '80%',
|
|
41
|
+
maxWidth: 360,
|
|
42
|
+
},
|
|
43
|
+
item: {
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
columnGap: 16,
|
|
46
|
+
flexDirection: 'row',
|
|
47
|
+
paddingHorizontal: 24,
|
|
48
|
+
paddingVertical: 16,
|
|
49
|
+
},
|
|
50
|
+
itemText: {
|
|
51
|
+
fontSize: 17,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// The drawer frame: a header button that opens the app's drawer menu over the screen, each item
|
|
56
|
+
// with its icon drawn from the icon data the root carries. Picking an item replaces the stack with
|
|
57
|
+
// that page.
|
|
58
|
+
function MenuButton({ items }) {
|
|
59
|
+
const [open, setOpen] = useState(false);
|
|
60
|
+
const { root } = useSessionState();
|
|
61
|
+
const insets = useSafeAreaInsets();
|
|
62
|
+
const tokens = useSurfaceTokens();
|
|
63
|
+
function openPage(pageId) {
|
|
64
|
+
setOpen(false);
|
|
65
|
+
const path = root.pageIndex.find((entry) => entry.pageId === pageId)?.path;
|
|
66
|
+
if (router.canDismiss()) router.dismissAll();
|
|
67
|
+
router.replace(`/${path}`);
|
|
68
|
+
}
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
<Pressable
|
|
72
|
+
accessibilityLabel="Menu"
|
|
73
|
+
accessibilityRole="button"
|
|
74
|
+
onPress={() => setOpen(true)}
|
|
75
|
+
style={styles.button}
|
|
76
|
+
testID="shell-menu"
|
|
77
|
+
>
|
|
78
|
+
<Text style={[styles.buttonText, { color: tokens.colorTint }]}>Menu</Text>
|
|
79
|
+
</Pressable>
|
|
80
|
+
<Modal animationType="fade" onRequestClose={() => setOpen(false)} transparent visible={open}>
|
|
81
|
+
<View style={styles.backdrop}>
|
|
82
|
+
<ScrollView
|
|
83
|
+
style={[
|
|
84
|
+
styles.sheet,
|
|
85
|
+
{ backgroundColor: tokens.colorGroupedBackground, paddingTop: insets.top },
|
|
86
|
+
]}
|
|
87
|
+
>
|
|
88
|
+
{items.map((item) => (
|
|
89
|
+
<Pressable
|
|
90
|
+
accessibilityRole="button"
|
|
91
|
+
key={item.pageId}
|
|
92
|
+
onPress={() => openPage(item.pageId)}
|
|
93
|
+
style={styles.item}
|
|
94
|
+
testID={`menu-${item.pageId}`}
|
|
95
|
+
>
|
|
96
|
+
{item.iconData ? (
|
|
97
|
+
<Icon
|
|
98
|
+
color={tokens.colorLabel}
|
|
99
|
+
data={item.iconData}
|
|
100
|
+
size={22}
|
|
101
|
+
testID={`menu-${item.pageId}-icon`}
|
|
102
|
+
/>
|
|
103
|
+
) : null}
|
|
104
|
+
<Text style={[styles.itemText, { color: tokens.colorLabel }]}>
|
|
105
|
+
{item.title ?? item.pageId}
|
|
106
|
+
</Text>
|
|
107
|
+
</Pressable>
|
|
108
|
+
))}
|
|
109
|
+
</ScrollView>
|
|
110
|
+
<Pressable
|
|
111
|
+
accessibilityLabel="Close menu"
|
|
112
|
+
onPress={() => setOpen(false)}
|
|
113
|
+
style={{ flex: 1 }}
|
|
114
|
+
/>
|
|
115
|
+
</View>
|
|
116
|
+
</Modal>
|
|
117
|
+
</>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default MenuButton;
|