@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,342 @@
|
|
|
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 { getSlotIndex } from './frameSlots.js';
|
|
20
|
+
import getFrame from './getFrame.js';
|
|
21
|
+
import getPresentation from './getPresentation.js';
|
|
22
|
+
import getSlotRootPath from './getSlotRootPath.js';
|
|
23
|
+
import getStackOptions from './getStackOptions.js';
|
|
24
|
+
import getTabHref from './getTabHref.js';
|
|
25
|
+
import getTabIcons from './getTabIcons.js';
|
|
26
|
+
import isPageUnderHeader from './isPageUnderHeader.js';
|
|
27
|
+
import toRouterHref from './toRouterHref.js';
|
|
28
|
+
import { createRoot } from '../test/fixtures.js';
|
|
29
|
+
|
|
30
|
+
const tabsRoot = createRoot();
|
|
31
|
+
|
|
32
|
+
test('getFrame reads tabs, drawer and stack frames', () => {
|
|
33
|
+
expect(getFrame(tabsRoot.menus).kind).toBe('tabs');
|
|
34
|
+
expect(getFrame({ drawer: [{ pageId: 'today' }] })).toEqual({
|
|
35
|
+
kind: 'drawer',
|
|
36
|
+
items: [{ pageId: 'today' }],
|
|
37
|
+
});
|
|
38
|
+
expect(getFrame({ stack: true })).toEqual({ kind: 'stack', items: [] });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('getFrame falls back to a stack when the caller may open fewer than two tabs', () => {
|
|
42
|
+
expect(getFrame({ tabs: [{ pageId: 'today' }] }).kind).toBe('stack');
|
|
43
|
+
expect(getFrame({ drawer: [] }).kind).toBe('stack');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('getSlotIndex reads the frame group of a route segment', () => {
|
|
47
|
+
expect(getSlotIndex('(t0)')).toBe(0);
|
|
48
|
+
expect(getSlotIndex('(t3)')).toBe(3);
|
|
49
|
+
expect(getSlotIndex('jobs')).toBe(-1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('getSlotRootPath gives each tab its page and a stack the home page', () => {
|
|
53
|
+
expect(getSlotRootPath({ root: tabsRoot, slotIndex: 0 })).toBe('today');
|
|
54
|
+
expect(getSlotRootPath({ root: tabsRoot, slotIndex: 1 })).toBe('jobs');
|
|
55
|
+
expect(getSlotRootPath({ root: tabsRoot, slotIndex: 4 })).toBeNull();
|
|
56
|
+
const stackRoot = createRoot({ menus: { stack: true }, homePageId: 'jobs' });
|
|
57
|
+
expect(getSlotRootPath({ root: stackRoot, slotIndex: 0 })).toBe('jobs');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('toRouterHref switches tabs for a link to a tab page and keeps the query', () => {
|
|
61
|
+
expect(toRouterHref({ pathname: '/jobs', query: 'a=1', root: tabsRoot })).toBe('/(t1)?a=1');
|
|
62
|
+
expect(toRouterHref({ pathname: '/jobs/12', query: '', root: tabsRoot })).toBe('/jobs/12');
|
|
63
|
+
const stackRoot = createRoot({ menus: { stack: true } });
|
|
64
|
+
expect(toRouterHref({ pathname: '/jobs', query: 'a=1', root: stackRoot })).toBe('/jobs?a=1');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('getTabHref names the tab that shows a page, and no tab for other pages and frames', () => {
|
|
68
|
+
expect(getTabHref({ path: 'today', query: '', root: tabsRoot })).toBe('/(t0)');
|
|
69
|
+
expect(getTabHref({ path: 'jobs', query: 'a=1', root: tabsRoot })).toBe('/(t1)?a=1');
|
|
70
|
+
expect(getTabHref({ path: 'jobs/12', query: '', root: tabsRoot })).toBeNull();
|
|
71
|
+
const stackRoot = createRoot({ menus: { stack: true } });
|
|
72
|
+
expect(getTabHref({ path: 'jobs', query: '', root: stackRoot })).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('getPresentation maps Screen presentations to native stack presentations', () => {
|
|
76
|
+
expect(getPresentation('card')).toBe('card');
|
|
77
|
+
expect(getPresentation('modal')).toBe('modal');
|
|
78
|
+
expect(getPresentation('sheet')).toBe('formSheet');
|
|
79
|
+
expect(getPresentation(undefined)).toBe('card');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function headerAction(overrides) {
|
|
83
|
+
return {
|
|
84
|
+
disabled: false,
|
|
85
|
+
id: 'add',
|
|
86
|
+
menu: null,
|
|
87
|
+
onPress: jest.fn(),
|
|
88
|
+
placement: 'right',
|
|
89
|
+
render: () => 'icon button',
|
|
90
|
+
sfSymbol: 'plus',
|
|
91
|
+
title: 'Add',
|
|
92
|
+
...overrides,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const screenOptions = {
|
|
97
|
+
headerActions: [],
|
|
98
|
+
headerLeft: undefined,
|
|
99
|
+
headerRight: undefined,
|
|
100
|
+
headerSearch: undefined,
|
|
101
|
+
largeTitle: false,
|
|
102
|
+
presentation: 'card',
|
|
103
|
+
title: 'Designs',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
test('getStackOptions gives a large title, the presentation and a native search bar', () => {
|
|
107
|
+
const headerSearch = {
|
|
108
|
+
autoFocus: true,
|
|
109
|
+
hideWhenScrolling: false,
|
|
110
|
+
onCancel: jest.fn(),
|
|
111
|
+
onChangeText: jest.fn(),
|
|
112
|
+
onSearch: jest.fn(),
|
|
113
|
+
placeholder: 'Search designs',
|
|
114
|
+
};
|
|
115
|
+
const searchBarRef = { current: null };
|
|
116
|
+
const options = getStackOptions({
|
|
117
|
+
options: { ...screenOptions, headerSearch, largeTitle: true, presentation: 'sheet' },
|
|
118
|
+
platform: 'ios',
|
|
119
|
+
searchBarRef,
|
|
120
|
+
});
|
|
121
|
+
expect(options).toMatchObject({
|
|
122
|
+
headerLargeTitleEnabled: true,
|
|
123
|
+
presentation: 'formSheet',
|
|
124
|
+
title: 'Designs',
|
|
125
|
+
});
|
|
126
|
+
// No header items: the sides are left as the stack has them (a drawer's menu button).
|
|
127
|
+
expect(options).not.toHaveProperty('headerLeft');
|
|
128
|
+
expect(options).not.toHaveProperty('headerRight');
|
|
129
|
+
const search = options.headerSearchBarOptions;
|
|
130
|
+
expect(search).toMatchObject({ placeholder: 'Search designs', ref: searchBarRef });
|
|
131
|
+
search.onChangeText({ nativeEvent: { text: 'dar' } });
|
|
132
|
+
search.onSearchButtonPress({ nativeEvent: { text: 'dark' } });
|
|
133
|
+
search.onCancelButtonPress({});
|
|
134
|
+
expect(headerSearch.onChangeText).toHaveBeenCalledWith('dar');
|
|
135
|
+
expect(headerSearch.onSearch).toHaveBeenCalledWith('dark');
|
|
136
|
+
expect(headerSearch.onCancel).toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('getStackOptions draws header actions as native SF Symbol bar buttons on iOS', () => {
|
|
140
|
+
const slot = () => 'slot blocks';
|
|
141
|
+
const add = headerAction();
|
|
142
|
+
const squirrel = headerAction({ id: 'squirrel', placement: 'left', sfSymbol: null });
|
|
143
|
+
const options = getStackOptions({
|
|
144
|
+
options: { ...screenOptions, headerActions: [add, squirrel], headerRight: slot },
|
|
145
|
+
platform: 'ios',
|
|
146
|
+
searchBarRef: { current: null },
|
|
147
|
+
});
|
|
148
|
+
expect(options.headerRight).toBeUndefined();
|
|
149
|
+
expect(options.unstable_headerRightItems()).toEqual([
|
|
150
|
+
{ type: 'custom', element: 'slot blocks' },
|
|
151
|
+
{
|
|
152
|
+
type: 'button',
|
|
153
|
+
accessibilityLabel: 'Add',
|
|
154
|
+
disabled: false,
|
|
155
|
+
icon: { type: 'sfSymbol', name: 'plus' },
|
|
156
|
+
identifier: 'add',
|
|
157
|
+
label: 'Add',
|
|
158
|
+
onPress: add.onPress,
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
// An icon with no SF Symbol draws as its own icon button.
|
|
162
|
+
expect(options.unstable_headerLeftItems()).toEqual([{ type: 'custom', element: 'icon button' }]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('getStackOptions draws a header action with items as a native menu on iOS', () => {
|
|
166
|
+
const copy = { destructive: false, disabled: false, id: 'copy_link', onPress: jest.fn() };
|
|
167
|
+
const remove = { destructive: true, disabled: false, id: 'delete', onPress: jest.fn() };
|
|
168
|
+
const more = headerAction({
|
|
169
|
+
id: 'more',
|
|
170
|
+
menu: [
|
|
171
|
+
{ ...copy, symbol: 'link', title: 'Copy link' },
|
|
172
|
+
{ ...remove, symbol: null, title: 'Delete' },
|
|
173
|
+
],
|
|
174
|
+
sfSymbol: 'ellipsis',
|
|
175
|
+
title: 'More',
|
|
176
|
+
});
|
|
177
|
+
const options = getStackOptions({
|
|
178
|
+
options: { ...screenOptions, headerActions: [more] },
|
|
179
|
+
platform: 'ios',
|
|
180
|
+
searchBarRef: { current: null },
|
|
181
|
+
});
|
|
182
|
+
expect(options.unstable_headerRightItems()).toEqual([
|
|
183
|
+
{
|
|
184
|
+
type: 'menu',
|
|
185
|
+
accessibilityLabel: 'More',
|
|
186
|
+
disabled: false,
|
|
187
|
+
icon: { type: 'sfSymbol', name: 'ellipsis' },
|
|
188
|
+
identifier: 'more',
|
|
189
|
+
label: 'More',
|
|
190
|
+
menu: {
|
|
191
|
+
items: [
|
|
192
|
+
{
|
|
193
|
+
type: 'action',
|
|
194
|
+
destructive: false,
|
|
195
|
+
disabled: false,
|
|
196
|
+
icon: { type: 'sfSymbol', name: 'link' },
|
|
197
|
+
label: 'Copy link',
|
|
198
|
+
onPress: copy.onPress,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
type: 'action',
|
|
202
|
+
destructive: true,
|
|
203
|
+
disabled: false,
|
|
204
|
+
icon: undefined,
|
|
205
|
+
label: 'Delete',
|
|
206
|
+
onPress: remove.onPress,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
// An action with a menu stays a native menu when its icon has no SF Symbol.
|
|
213
|
+
const plain = getStackOptions({
|
|
214
|
+
options: { ...screenOptions, headerActions: [{ ...more, sfSymbol: null }] },
|
|
215
|
+
platform: 'ios',
|
|
216
|
+
searchBarRef: { current: null },
|
|
217
|
+
});
|
|
218
|
+
expect(plain.unstable_headerRightItems()[0]).toMatchObject({ type: 'menu', icon: undefined });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('getStackOptions draws a header slot alone as its custom view on iOS', () => {
|
|
222
|
+
const slot = () => 'slot blocks';
|
|
223
|
+
const options = getStackOptions({
|
|
224
|
+
options: { ...screenOptions, headerRight: slot },
|
|
225
|
+
platform: 'ios',
|
|
226
|
+
searchBarRef: { current: null },
|
|
227
|
+
});
|
|
228
|
+
expect(options.headerRight).toBe(slot);
|
|
229
|
+
expect(options.unstable_headerRightItems).toBeUndefined();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('getStackOptions hides the bar hairline for a headerBottom, which draws its own', () => {
|
|
233
|
+
const options = getStackOptions({
|
|
234
|
+
options: { ...screenOptions, headerBottom: true },
|
|
235
|
+
platform: 'ios',
|
|
236
|
+
searchBarRef: { current: null },
|
|
237
|
+
});
|
|
238
|
+
expect(options.headerShadowVisible).toBe(false);
|
|
239
|
+
// Without one, the stack keeps its own default.
|
|
240
|
+
expect(
|
|
241
|
+
getStackOptions({ options: screenOptions, platform: 'ios', searchBarRef: { current: null } })
|
|
242
|
+
.headerShadowVisible
|
|
243
|
+
).toBeUndefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('getStackOptions makes the iOS bar clear over a Screen with a backgroundTint', () => {
|
|
247
|
+
const tinted = { ...screenOptions, backgroundTint: true, largeTitle: true };
|
|
248
|
+
const searchBarRef = { current: null };
|
|
249
|
+
expect(
|
|
250
|
+
getStackOptions({ options: tinted, platform: 'ios', platformVersion: '26.0', searchBarRef })
|
|
251
|
+
).toMatchObject({ headerBlurEffect: undefined, headerTransparent: true });
|
|
252
|
+
// Before iOS 26: clear at the scroll edge, the chrome material once content scrolls under it.
|
|
253
|
+
expect(
|
|
254
|
+
getStackOptions({ options: tinted, platform: 'ios', platformVersion: '18.4', searchBarRef })
|
|
255
|
+
).toMatchObject({
|
|
256
|
+
headerBlurEffect: 'systemChromeMaterial',
|
|
257
|
+
headerLargeStyle: { backgroundColor: 'transparent' },
|
|
258
|
+
headerTransparent: true,
|
|
259
|
+
});
|
|
260
|
+
// Untinted, and off iOS, the stack's own bar: set undefined, as setOptions merges.
|
|
261
|
+
const untinted = getStackOptions({
|
|
262
|
+
options: screenOptions,
|
|
263
|
+
platform: 'ios',
|
|
264
|
+
platformVersion: '26.0',
|
|
265
|
+
searchBarRef,
|
|
266
|
+
});
|
|
267
|
+
expect(untinted).toHaveProperty('headerTransparent', undefined);
|
|
268
|
+
expect(
|
|
269
|
+
getStackOptions({ options: tinted, platform: 'android', platformVersion: 35, searchBarRef })
|
|
270
|
+
.headerTransparent
|
|
271
|
+
).toBeUndefined();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('isPageUnderHeader: a Screen with a backgroundTint scrolls under its clear bar on iOS', () => {
|
|
275
|
+
expect(
|
|
276
|
+
isPageUnderHeader({
|
|
277
|
+
options: { backgroundTint: true },
|
|
278
|
+
platform: 'ios',
|
|
279
|
+
platformVersion: '18.4',
|
|
280
|
+
})
|
|
281
|
+
).toBe(true);
|
|
282
|
+
expect(
|
|
283
|
+
isPageUnderHeader({
|
|
284
|
+
options: { backgroundTint: true },
|
|
285
|
+
platform: 'android',
|
|
286
|
+
platformVersion: 35,
|
|
287
|
+
})
|
|
288
|
+
).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('isPageUnderHeader: iOS 26 pages scroll under the glass bar', () => {
|
|
292
|
+
expect(isPageUnderHeader({ options: {}, platform: 'ios', platformVersion: '26.0' })).toBe(true);
|
|
293
|
+
expect(isPageUnderHeader({ options: {}, platform: 'ios', platformVersion: '27.1' })).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('isPageUnderHeader: before iOS 26 only a large title or search bar makes the bar translucent', () => {
|
|
297
|
+
const ios18 = { platform: 'ios', platformVersion: '18.4' };
|
|
298
|
+
expect(isPageUnderHeader({ ...ios18, options: { largeTitle: false } })).toBe(false);
|
|
299
|
+
expect(isPageUnderHeader({ ...ios18, options: { largeTitle: true } })).toBe(true);
|
|
300
|
+
expect(isPageUnderHeader({ ...ios18, options: { headerSearch: { placeholder: 'Find' } } })).toBe(
|
|
301
|
+
true
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('isPageUnderHeader: Android pages sit below the header', () => {
|
|
306
|
+
expect(
|
|
307
|
+
isPageUnderHeader({ options: { largeTitle: true }, platform: 'android', platformVersion: 35 })
|
|
308
|
+
).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('getStackOptions draws header actions as a row of icon buttons off iOS', () => {
|
|
312
|
+
const options = getStackOptions({
|
|
313
|
+
options: { ...screenOptions, headerActions: [headerAction()] },
|
|
314
|
+
platform: 'android',
|
|
315
|
+
searchBarRef: { current: null },
|
|
316
|
+
});
|
|
317
|
+
expect(options).not.toHaveProperty('unstable_headerRightItems');
|
|
318
|
+
const row = options.headerRight();
|
|
319
|
+
expect(row.props.children).toEqual([null, ['icon button']]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('getTabIcons gives SF Symbols on iOS and Lucide icon data elsewhere', () => {
|
|
323
|
+
const inbox = { node: [['path', { d: 'M1 1' }]] };
|
|
324
|
+
const squirrel = { node: [['path', { d: 'M2 2' }]] };
|
|
325
|
+
const items = [
|
|
326
|
+
{ pageId: 'my-work', icon: 'Inbox', iconData: inbox },
|
|
327
|
+
{ pageId: 'designs', icon: 'FileText', iconData: {}, sfSymbol: 'doc.richtext' },
|
|
328
|
+
{ pageId: 'pets', icon: 'Squirrel', iconData: squirrel },
|
|
329
|
+
{ pageId: 'plain' },
|
|
330
|
+
];
|
|
331
|
+
expect(getTabIcons({ items, platform: 'ios' })).toEqual([
|
|
332
|
+
{ pageId: 'my-work', sf: 'tray', iconData: null },
|
|
333
|
+
{ pageId: 'designs', sf: 'doc.richtext', iconData: null },
|
|
334
|
+
{ pageId: 'pets', sf: null, iconData: squirrel },
|
|
335
|
+
{ pageId: 'plain', sf: null, iconData: null },
|
|
336
|
+
]);
|
|
337
|
+
expect(getTabIcons({ items, platform: 'android' })[0]).toEqual({
|
|
338
|
+
pageId: 'my-work',
|
|
339
|
+
sf: null,
|
|
340
|
+
iconData: inbox,
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The page path of a link to the app's own site (a universal link or Android app link), with its
|
|
19
|
+
* query: https://field.example.com/acme/tasks/42?tab=notes under basePath /acme is
|
|
20
|
+
* /tasks/42?tab=notes. Any other link is returned as it is.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} params
|
|
23
|
+
* @param {string} params.appUrl The app's url for the shell's environment.
|
|
24
|
+
* @param {string} params.path The link the system opened the shell with.
|
|
25
|
+
*/
|
|
26
|
+
function toAppLinkPath({ appUrl, path }) {
|
|
27
|
+
if (!/^https?:\/\//.test(path ?? '')) {
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
const app = new URL(appUrl);
|
|
31
|
+
const link = new URL(path);
|
|
32
|
+
const basePath = app.pathname.replace(/\/$/, '');
|
|
33
|
+
// Under the base path itself: /acme or /acme/..., not /acmeevil/...
|
|
34
|
+
const underBasePath =
|
|
35
|
+
link.pathname === basePath || link.pathname.startsWith(`${basePath}/`) || basePath === '';
|
|
36
|
+
if (link.host !== app.host || !underBasePath) {
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
const pagePath = link.pathname.slice(basePath.length) || '/';
|
|
40
|
+
return `${pagePath.startsWith('/') ? pagePath : `/${pagePath}`}${link.search}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default toAppLinkPath;
|
|
@@ -0,0 +1,53 @@
|
|
|
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 toAppLinkPath from './toAppLinkPath.js';
|
|
18
|
+
|
|
19
|
+
test('toAppLinkPath turns a link to the app into its page path and query', () => {
|
|
20
|
+
expect(
|
|
21
|
+
toAppLinkPath({
|
|
22
|
+
appUrl: 'https://field.example.com/acme',
|
|
23
|
+
path: 'https://field.example.com/acme/tasks/42?tab=notes',
|
|
24
|
+
})
|
|
25
|
+
).toBe('/tasks/42?tab=notes');
|
|
26
|
+
expect(
|
|
27
|
+
toAppLinkPath({ appUrl: 'https://field.example.com', path: 'https://field.example.com' })
|
|
28
|
+
).toBe('/');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('toAppLinkPath leaves other links alone', () => {
|
|
32
|
+
expect(
|
|
33
|
+
toAppLinkPath({ appUrl: 'https://field.example.com', path: 'https://other.example.com/x' })
|
|
34
|
+
).toBe('https://other.example.com/x');
|
|
35
|
+
expect(toAppLinkPath({ appUrl: 'https://field.example.com', path: 'acme://tasks/1' })).toBe(
|
|
36
|
+
'acme://tasks/1'
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('toAppLinkPath leaves a link that only starts like the base path alone', () => {
|
|
41
|
+
expect(
|
|
42
|
+
toAppLinkPath({
|
|
43
|
+
appUrl: 'https://field.example.com/acme',
|
|
44
|
+
path: 'https://field.example.com/acmeevil/tasks/1',
|
|
45
|
+
})
|
|
46
|
+
).toBe('https://field.example.com/acmeevil/tasks/1');
|
|
47
|
+
expect(
|
|
48
|
+
toAppLinkPath({
|
|
49
|
+
appUrl: 'https://field.example.com/acme/',
|
|
50
|
+
path: 'https://field.example.com/acme',
|
|
51
|
+
})
|
|
52
|
+
).toBe('/');
|
|
53
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
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 getTabHref from './getTabHref.js';
|
|
18
|
+
|
|
19
|
+
// The router href for a link the core follows ({ pathname, query }). A link to a tab's own page
|
|
20
|
+
// switches to that tab, rather than stacking another copy of the page on the current one.
|
|
21
|
+
function toRouterHref({ pathname, query, root }) {
|
|
22
|
+
const tabHref = getTabHref({ path: pathname.replace(/^\//, ''), query, root });
|
|
23
|
+
if (tabHref !== null) {
|
|
24
|
+
return tabHref;
|
|
25
|
+
}
|
|
26
|
+
return `${pathname}${query ? `?${query}` : ''}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default toRouterHref;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as Notifications from 'expo-notifications';
|
|
18
|
+
import { type } from '@lowdefy/helpers';
|
|
19
|
+
|
|
20
|
+
import { getCurrentApp } from '../native/currentApp.js';
|
|
21
|
+
import registerPushToken from './registerPushToken.js';
|
|
22
|
+
import reportShellError from '../host/reportShellError.js';
|
|
23
|
+
import toScheduleRequest from './toScheduleRequest.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The notifications device feature the native actions use (ScheduleNotification,
|
|
27
|
+
* CancelNotification, SetBadgeCount, RequestNotificationPermission).
|
|
28
|
+
*/
|
|
29
|
+
function createNotificationsFeature() {
|
|
30
|
+
return {
|
|
31
|
+
async schedule(params) {
|
|
32
|
+
const id = await Notifications.scheduleNotificationAsync(toScheduleRequest(params));
|
|
33
|
+
return { id };
|
|
34
|
+
},
|
|
35
|
+
async cancel({ all, id }) {
|
|
36
|
+
if (all === true) {
|
|
37
|
+
await Notifications.cancelAllScheduledNotificationsAsync();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!type.isString(id)) {
|
|
41
|
+
throw new Error('CancelNotification requires "id", or "all: true".');
|
|
42
|
+
}
|
|
43
|
+
await Notifications.cancelScheduledNotificationAsync(id);
|
|
44
|
+
},
|
|
45
|
+
async setBadgeCount({ count }) {
|
|
46
|
+
await Notifications.setBadgeCountAsync(count);
|
|
47
|
+
},
|
|
48
|
+
async requestPermission() {
|
|
49
|
+
const { granted, status } = await Notifications.requestPermissionsAsync();
|
|
50
|
+
if (granted) {
|
|
51
|
+
const app = await getCurrentApp();
|
|
52
|
+
registerPushToken({ app }).catch((error) =>
|
|
53
|
+
reportShellError({ error, session: app.session })
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return { granted, status };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default createNotificationsFeature;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The mobile.notifications category action a response names, or null for a tap on the
|
|
19
|
+
* notification itself (or an action the config no longer has).
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} params
|
|
22
|
+
* @param {Object} params.notifications The root's notifications config.
|
|
23
|
+
* @param {string|null} params.categoryId
|
|
24
|
+
* @param {string} params.actionId
|
|
25
|
+
*/
|
|
26
|
+
function getCategoryAction({ actionId, categoryId, notifications }) {
|
|
27
|
+
const category = (notifications?.categories ?? []).find((item) => item.id === categoryId);
|
|
28
|
+
return category?.actions.find((action) => action.id === actionId) ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default getCategoryAction;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as Notifications from 'expo-notifications';
|
|
18
|
+
|
|
19
|
+
import callEndpoint from '../native/callEndpoint.js';
|
|
20
|
+
import getCategoryAction from './getCategoryAction.js';
|
|
21
|
+
import openPath from '../native/openPath.js';
|
|
22
|
+
import toNotificationEvent from './toNotificationEvent.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* What a tap or a button on a notification does:
|
|
26
|
+
* - a button with `snooze`: shows the same notification again that many minutes later, on the
|
|
27
|
+
* phone alone;
|
|
28
|
+
* - a button with `endpointId` (Approve, or Reply with its typed text): calls the endpoint with
|
|
29
|
+
* { actionId, categoryId, text, data, notification }, with the app in the background;
|
|
30
|
+
* - a tap, or any other button: opens `data.path` (for a tap) and fires onNotificationAction with
|
|
31
|
+
* { actionId (default for a tap), categoryId, text, data, notification }.
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} params
|
|
34
|
+
* @param {Object} params.app The open app (currentApp.js).
|
|
35
|
+
* @param {{ actionIdentifier: string, notification: Object, userText?: string }} params.response
|
|
36
|
+
*/
|
|
37
|
+
async function handleNotificationResponse({ app, response }) {
|
|
38
|
+
const notification = toNotificationEvent(response.notification);
|
|
39
|
+
const tapped = response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER;
|
|
40
|
+
const actionId = tapped ? 'default' : response.actionIdentifier;
|
|
41
|
+
const event = {
|
|
42
|
+
actionId,
|
|
43
|
+
categoryId: notification.categoryId,
|
|
44
|
+
data: notification.data,
|
|
45
|
+
notification,
|
|
46
|
+
text: response.userText ?? null,
|
|
47
|
+
};
|
|
48
|
+
const action = tapped
|
|
49
|
+
? null
|
|
50
|
+
: getCategoryAction({
|
|
51
|
+
actionId,
|
|
52
|
+
categoryId: notification.categoryId,
|
|
53
|
+
notifications: app.session.getState().root?.notifications,
|
|
54
|
+
});
|
|
55
|
+
if (action !== null && action.snooze !== null) {
|
|
56
|
+
const { content } = response.notification.request;
|
|
57
|
+
await Notifications.scheduleNotificationAsync({
|
|
58
|
+
content: {
|
|
59
|
+
title: content.title,
|
|
60
|
+
body: content.body,
|
|
61
|
+
data: content.data,
|
|
62
|
+
categoryIdentifier: content.categoryIdentifier,
|
|
63
|
+
},
|
|
64
|
+
trigger: {
|
|
65
|
+
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
|
|
66
|
+
seconds: action.snooze * 60,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (action !== null && action.endpointId !== null) {
|
|
72
|
+
await callEndpoint({ app, endpointId: action.endpointId, payload: event });
|
|
73
|
+
await Notifications.dismissNotificationAsync(notification.id);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (tapped) {
|
|
77
|
+
openPath({ app, path: notification.data.path });
|
|
78
|
+
}
|
|
79
|
+
await app.triggerAppEvent({ name: 'onNotificationAction', event });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default handleNotificationResponse;
|