@lowdefy/shell 3.23.3 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. package/LICENSE +183 -183
  2. package/README.md +364 -2
  3. package/app/(t0,t1,t2,t3,t4)/[...path].js +38 -0
  4. package/app/(t0,t1,t2,t3,t4)/_layout.js +48 -0
  5. package/app/(t0,t1,t2,t3,t4)/index.js +38 -0
  6. package/app/+native-intent.js +39 -0
  7. package/app/_layout.js +36 -0
  8. package/app.config.ts +183 -0
  9. package/appLinks.js +65 -0
  10. package/applyShellPlugins.js +43 -0
  11. package/authCallbackIntentFilters.js +46 -0
  12. package/babel.config.js +25 -0
  13. package/checkShellConfig.js +172 -0
  14. package/checkShellPlugins.js +74 -0
  15. package/getPrivacyManifest.js +39 -0
  16. package/metro.config.js +73 -0
  17. package/native/LowdefyAppShortcuts.swift.template +84 -0
  18. package/native/withAppShortcuts.js +60 -0
  19. package/native-fingerprint.json +16 -0
  20. package/package.json +120 -40
  21. package/profiles.js +607 -0
  22. package/readShellConfig.js +36 -0
  23. package/resolveShellPlugins.js +109 -0
  24. package/runtimeVersion.js +112 -0
  25. package/scripts/checkReleaseBundle.mjs +358 -0
  26. package/scripts/createDayjsLocales.mjs +67 -0
  27. package/scripts/createShellAssets.mjs +54 -0
  28. package/scripts/createShellVersion.mjs +83 -0
  29. package/scripts/createTypeManifest.mjs +205 -0
  30. package/shell.dev.json +11 -0
  31. package/shellVariant.js +126 -0
  32. package/src/ShellContext.js +32 -0
  33. package/src/actions/CopyToClipboard.js +29 -0
  34. package/src/actions/CopyToClipboard.test.js +33 -0
  35. package/src/actions/GeolocationCurrentPosition.js +61 -0
  36. package/src/actions/GeolocationCurrentPosition.test.js +79 -0
  37. package/src/actions/GetLocalStorage.js +36 -0
  38. package/src/actions/RemoveLocalStorage.js +31 -0
  39. package/src/actions/ScrollTo.js +66 -0
  40. package/src/actions/ScrollTo.test.js +83 -0
  41. package/src/actions/SetFocus.js +33 -0
  42. package/src/actions/SetFocus.test.js +49 -0
  43. package/src/actions/SetLocalStorage.js +34 -0
  44. package/src/actions/localStorage.test.js +68 -0
  45. package/src/actions/measureInScrollView.js +36 -0
  46. package/src/actions/nativeActions.js +41 -0
  47. package/src/auth/AuthButton.js +44 -0
  48. package/src/auth/AuthGate.js +57 -0
  49. package/src/auth/AuthLink.js +43 -0
  50. package/src/auth/LockScreen.js +67 -0
  51. package/src/auth/LockScreen.test.js +37 -0
  52. package/src/auth/SignInScreen.js +268 -0
  53. package/src/auth/SignInScreen.test.js +187 -0
  54. package/src/auth/createAppAuth.js +325 -0
  55. package/src/auth/createAppAuth.test.js +424 -0
  56. package/src/auth/createAuthApi.js +115 -0
  57. package/src/auth/createAuthApi.test.js +138 -0
  58. package/src/auth/createCoreAuth.js +96 -0
  59. package/src/auth/createCoreAuth.test.js +188 -0
  60. package/src/auth/createDeviceKey.js +62 -0
  61. package/src/auth/createDeviceKey.test.js +54 -0
  62. package/src/auth/createSessionStores.js +45 -0
  63. package/src/auth/createTokenStore.js +103 -0
  64. package/src/auth/createTokenStore.test.js +116 -0
  65. package/src/auth/getAuthSettings.js +24 -0
  66. package/src/auth/getAuthView.js +39 -0
  67. package/src/auth/getAuthView.test.js +52 -0
  68. package/src/auth/getSheetReturnUrl.js +35 -0
  69. package/src/auth/getTokenStoreOptions.js +46 -0
  70. package/src/auth/isBackgroundShell.js +26 -0
  71. package/src/auth/isSheetReturnLink.js +35 -0
  72. package/src/auth/parseQuery.js +39 -0
  73. package/src/auth/purgeAppData.js +80 -0
  74. package/src/auth/purgeAppData.test.js +104 -0
  75. package/src/auth/shouldLock.js +26 -0
  76. package/src/auth/signInWithSheet.js +97 -0
  77. package/src/auth/signInWithSheet.test.js +205 -0
  78. package/src/auth/styles.js +78 -0
  79. package/src/auth/unregisterPushToken.js +62 -0
  80. package/src/auth/unregisterPushToken.test.js +67 -0
  81. package/src/auth/usesBiometricUnlock.js +23 -0
  82. package/src/auth/watchSession.js +47 -0
  83. package/src/auth/withoutCookies.js +26 -0
  84. package/src/cache/createSqliteCacheStore.js +113 -0
  85. package/src/cache/createSqliteCacheStore.test.js +93 -0
  86. package/src/cache/getCacheKey.js +25 -0
  87. package/src/cache/getLastCacheKeyName.js +22 -0
  88. package/src/components/AppProvider.js +118 -0
  89. package/src/components/Frame.js +116 -0
  90. package/src/components/Frame.test.js +101 -0
  91. package/src/components/LoadingScreen.js +35 -0
  92. package/src/components/MenuButton.js +121 -0
  93. package/src/components/MenuButton.test.js +74 -0
  94. package/src/components/MessageScreen.js +51 -0
  95. package/src/components/NavigationTheme.js +33 -0
  96. package/src/components/NoConnectionBanner.js +51 -0
  97. package/src/components/NoConnectionBanner.test.js +73 -0
  98. package/src/components/OtherPageScreen.js +95 -0
  99. package/src/components/OtherPageScreen.test.js +74 -0
  100. package/src/components/PageScreen.js +254 -0
  101. package/src/components/PageScreen.test.js +492 -0
  102. package/src/components/RouteErrorScreen.js +47 -0
  103. package/src/components/ShellRoot.js +34 -0
  104. package/src/components/ShellRoot.test.js +62 -0
  105. package/src/components/ShellThemeProvider.js +45 -0
  106. package/src/components/SwitchToTab.js +36 -0
  107. package/src/components/SwitchToTab.test.js +63 -0
  108. package/src/components/TabIconImages.js +72 -0
  109. package/src/components/UpdateScreen.js +40 -0
  110. package/src/components/getNavigationTheme.js +44 -0
  111. package/src/components/getNavigationTheme.test.js +41 -0
  112. package/src/components/styles.js +50 -0
  113. package/src/components/useSurfaceTokens.js +47 -0
  114. package/src/config/getAppApiBase.js +34 -0
  115. package/src/config/getShellConfig.js +37 -0
  116. package/src/config/getShellConfig.test.js +80 -0
  117. package/src/createShellApp.js +126 -0
  118. package/src/dev/DevShell.js +239 -0
  119. package/src/dev/DevShell.test.js +233 -0
  120. package/src/dev/connect/ConfirmSheet.js +71 -0
  121. package/src/dev/connect/ConnectScreen.js +117 -0
  122. package/src/dev/connect/QrScanner.js +89 -0
  123. package/src/dev/connection/createDevShellConfig.js +47 -0
  124. package/src/dev/connection/createDevTransport.js +51 -0
  125. package/src/dev/connection/createDevTransport.test.js +87 -0
  126. package/src/dev/connection/createServerList.js +48 -0
  127. package/src/dev/connection/isLoopbackHost.js +25 -0
  128. package/src/dev/connection/pairedShellApp.test.js +104 -0
  129. package/src/dev/connection/parseDevLink.js +66 -0
  130. package/src/dev/connection/parseDevLink.test.js +97 -0
  131. package/src/dev/connection/parseQuery.js +33 -0
  132. package/src/dev/connection/parseServerInput.js +40 -0
  133. package/src/dev/connection/parseServerUrl.js +45 -0
  134. package/src/dev/devStyles.js +97 -0
  135. package/src/dev/index.js +21 -0
  136. package/src/dev/inject/createDeviceInjections.js +188 -0
  137. package/src/dev/inject/createDeviceInjections.test.js +224 -0
  138. package/src/dev/inject/createOfflineFetch.js +35 -0
  139. package/src/dev/inject/createOfflineFetch.test.js +68 -0
  140. package/src/dev/inject/injectPush.js +60 -0
  141. package/src/dev/inject/injectShare.js +55 -0
  142. package/src/dev/inject/withInjectedDevice.js +98 -0
  143. package/src/dev/menu/BlockGallery.js +33 -0
  144. package/src/dev/menu/DevMenu.js +121 -0
  145. package/src/dev/menu/createLogBuffer.js +83 -0
  146. package/src/dev/menu/watchShake.js +52 -0
  147. package/src/dev/menu/watchShake.test.js +61 -0
  148. package/src/dev/recorder/createNativeRecorder.js +204 -0
  149. package/src/dev/recorder/createNativeRecorder.test.js +307 -0
  150. package/src/dev/redirectDevLink.js +30 -0
  151. package/src/dev/redirectDevLink.test.js +49 -0
  152. package/src/dev/steps/createStepRunner.js +184 -0
  153. package/src/dev/steps/createStepRunner.test.js +262 -0
  154. package/src/dev/steps/findBlock.js +78 -0
  155. package/src/dev/steps/isContextSettled.js +37 -0
  156. package/src/dev/steps/waitForSettle.js +39 -0
  157. package/src/dev/steps/waitUntil.js +40 -0
  158. package/src/dev/tab/DevTabClient.js +97 -0
  159. package/src/dev/tab/answerEval.js +39 -0
  160. package/src/dev/tab/answerInspect.js +45 -0
  161. package/src/dev/tab/createDevTabClient.js +272 -0
  162. package/src/dev/tab/createDevTabClient.test.js +317 -0
  163. package/src/dev/tab/describeTabError.js +35 -0
  164. package/src/dev/tab/devClientMarker.js +22 -0
  165. package/src/dev/tab/devClientMarker.test.js +23 -0
  166. package/src/dev/tab/findPageContext.js +43 -0
  167. package/src/dev/tab/getShownPage.js +42 -0
  168. package/src/dev/tab/readEventStream.js +68 -0
  169. package/src/dev/tab/reloadApp.js +42 -0
  170. package/src/dev/tab/reloadApp.test.js +115 -0
  171. package/src/device/DeviceHosts.js +32 -0
  172. package/src/device/basic/createFilesFeature.js +41 -0
  173. package/src/device/basic/createHapticsFeature.js +46 -0
  174. package/src/device/basic/createKeepAwakeFeature.js +37 -0
  175. package/src/device/basic/createPhotosFeature.js +40 -0
  176. package/src/device/basic/createRequestQueueFeature.js +37 -0
  177. package/src/device/basic/createShareFeature.js +63 -0
  178. package/src/device/basic/createShareFeature.test.js +47 -0
  179. package/src/device/basic/createSpeechFeature.js +43 -0
  180. package/src/device/basic/createUploadFeature.js +41 -0
  181. package/src/device/basic/toImagePickerOptions.js +41 -0
  182. package/src/device/basic/toPickedFile.js +54 -0
  183. package/src/device/calendar/index.js +68 -0
  184. package/src/device/contacts/index.js +67 -0
  185. package/src/device/createDevice.js +65 -0
  186. package/src/device/device.js +27 -0
  187. package/src/device/field/MapView.js +93 -0
  188. package/src/device/field/ScanCodeSheet.js +86 -0
  189. package/src/device/field/ScannerView.js +52 -0
  190. package/src/device/field/createAudioFeature.js +70 -0
  191. package/src/device/field/createBrightnessFeature.js +40 -0
  192. package/src/device/field/createCameraFeature.js +56 -0
  193. package/src/device/field/createDocumentScannerFeature.js +51 -0
  194. package/src/device/field/createLocationFeature.js +39 -0
  195. package/src/device/field/createScanRequests.js +55 -0
  196. package/src/device/field/index.js +39 -0
  197. package/src/device/field/scanRequests.js +22 -0
  198. package/src/device/injected.js +60 -0
  199. package/src/device/injected.test.js +39 -0
  200. package/src/device/nfc/index.js +89 -0
  201. package/src/device/nfc/toNfcRecord.js +44 -0
  202. package/src/device/nfc/toNfcRecord.test.js +48 -0
  203. package/src/device/tracking/createBacklog.js +63 -0
  204. package/src/device/tracking/createBacklog.test.js +47 -0
  205. package/src/device/tracking/index.js +211 -0
  206. package/src/device/tracking/index.test.js +110 -0
  207. package/src/host/createDeviceStore.js +32 -0
  208. package/src/host/createMemoryStore.js +32 -0
  209. package/src/host/createNetworkStatus.js +94 -0
  210. package/src/host/createNetworkStatus.test.js +94 -0
  211. package/src/host/createRouterAdapter.js +33 -0
  212. package/src/host/createShellFetch.js +62 -0
  213. package/src/host/createShellFetch.test.js +165 -0
  214. package/src/host/createShellGlobals.js +62 -0
  215. package/src/host/createShellHost.js +83 -0
  216. package/src/host/createShellHost.test.js +36 -0
  217. package/src/host/getOrigin.js +31 -0
  218. package/src/host/isNetworkError.js +24 -0
  219. package/src/host/isOnline.js +22 -0
  220. package/src/host/networkStatus.js +24 -0
  221. package/src/host/readServerError.js +45 -0
  222. package/src/host/readServerError.test.js +85 -0
  223. package/src/host/reportShellError.js +47 -0
  224. package/src/host/reportShellError.test.js +79 -0
  225. package/src/host/withNetworkErrors.js +38 -0
  226. package/src/host/withNetworkErrors.test.js +61 -0
  227. package/src/host/withShellHeader.js +25 -0
  228. package/src/host/withShellHeader.test.js +28 -0
  229. package/src/js/evaluateJsEntries.js +43 -0
  230. package/src/js/evaluateJsEntries.test.js +48 -0
  231. package/src/locale/createShellLocale.js +112 -0
  232. package/src/locale/createShellLocale.test.js +74 -0
  233. package/src/locale/dayjsLocales.js +165 -0
  234. package/src/locale/getDeviceLocales.js +23 -0
  235. package/src/locale/loadDayjsLocale.js +32 -0
  236. package/src/locale/resolveActiveLocale.js +46 -0
  237. package/src/locale/resolveActiveLocale.test.js +40 -0
  238. package/src/native/backgroundFetch.js +97 -0
  239. package/src/native/callEndpoint.js +63 -0
  240. package/src/native/createAppEventTrigger.js +70 -0
  241. package/src/native/currentApp.js +49 -0
  242. package/src/native/getPagePath.js +32 -0
  243. package/src/native/openPath.js +55 -0
  244. package/src/native/openPath.test.js +39 -0
  245. package/src/native/parseShellLink.js +38 -0
  246. package/src/native/parseShellLink.test.js +29 -0
  247. package/src/native/startAppServices.js +94 -0
  248. package/src/native/startAppServices.test.js +97 -0
  249. package/src/native/startShareIn.js +59 -0
  250. package/src/native/startShortcuts.js +88 -0
  251. package/src/native/startShortcuts.test.js +59 -0
  252. package/src/native/toSharePayload.js +47 -0
  253. package/src/native/toSharePayload.test.js +47 -0
  254. package/src/native/whenAppReady.js +48 -0
  255. package/src/navigation/SlotContext.js +24 -0
  256. package/src/navigation/frameSlots.js +27 -0
  257. package/src/navigation/getFrame.js +30 -0
  258. package/src/navigation/getInitialScreenOptions.js +63 -0
  259. package/src/navigation/getInitialScreenOptions.test.js +66 -0
  260. package/src/navigation/getPresentation.js +28 -0
  261. package/src/navigation/getSlotRootPath.js +27 -0
  262. package/src/navigation/getStackOptions.js +185 -0
  263. package/src/navigation/getTabHref.js +45 -0
  264. package/src/navigation/getTabIcons.js +43 -0
  265. package/src/navigation/isPageUnderHeader.js +46 -0
  266. package/src/navigation/navigation.test.js +342 -0
  267. package/src/navigation/toAppLinkPath.js +43 -0
  268. package/src/navigation/toAppLinkPath.test.js +53 -0
  269. package/src/navigation/toRouterHref.js +29 -0
  270. package/src/notifications/createNotificationsFeature.js +61 -0
  271. package/src/notifications/getCategoryAction.js +31 -0
  272. package/src/notifications/handleNotificationResponse.js +82 -0
  273. package/src/notifications/registerPushToken.js +71 -0
  274. package/src/notifications/startNotifications.js +114 -0
  275. package/src/notifications/toCategoryActions.js +42 -0
  276. package/src/notifications/toNotificationEvent.js +36 -0
  277. package/src/notifications/toScheduleRequest.js +62 -0
  278. package/src/plugins/appTypes.js +28 -0
  279. package/src/plugins/prefixTypes.js +28 -0
  280. package/src/plugins/shellPlugins.json +15 -0
  281. package/src/plugins/typeManifest.js +789 -0
  282. package/src/plugins/types.js +64 -0
  283. package/src/plugins/types.test.js +173 -0
  284. package/src/polyfills/installGetRandomValues.js +28 -0
  285. package/src/queue/createRequestQueue.js +198 -0
  286. package/src/queue/createRequestQueue.test.js +215 -0
  287. package/src/queue/createSqliteQueueStore.js +121 -0
  288. package/src/queue/summarizeQueue.js +39 -0
  289. package/src/session/createAppSession.js +323 -0
  290. package/src/session/createAppSession.test.js +391 -0
  291. package/src/session/getPageView.js +59 -0
  292. package/src/session/getPageView.test.js +137 -0
  293. package/src/sync/encodePagePath.js +27 -0
  294. package/src/sync/fetchPage.js +72 -0
  295. package/src/sync/fetchPage.test.js +84 -0
  296. package/src/sync/fetchRoot.js +43 -0
  297. package/src/sync/fetchUser.js +33 -0
  298. package/src/sync/runLimited.js +36 -0
  299. package/src/sync/toRootConfig.js +35 -0
  300. package/src/test/createFakeNetwork.js +40 -0
  301. package/src/test/createFakeSecureStore.js +40 -0
  302. package/src/test/createFakeServer.js +98 -0
  303. package/src/test/createMemoryCacheStore.js +59 -0
  304. package/src/test/createNetworkError.js +24 -0
  305. package/src/test/expoImage.js +42 -0
  306. package/src/test/expoNotifications.js +40 -0
  307. package/src/test/expoQuickActions.js +22 -0
  308. package/src/test/expoSpeechRecognition.js +31 -0
  309. package/src/test/expoTaskManager.js +25 -0
  310. package/src/test/expoUi.js +55 -0
  311. package/src/test/expoUiBottomSheet.js +64 -0
  312. package/src/test/expoUiDateTimePicker.js +26 -0
  313. package/src/test/expoUiMenu.js +46 -0
  314. package/src/test/expoUiSegmentedControl.js +47 -0
  315. package/src/test/expoUiSwiftUi.js +62 -0
  316. package/src/test/expoUiSwiftUiModifiers.js +23 -0
  317. package/src/test/expoWebBrowser.js +23 -0
  318. package/src/test/fixtures.js +98 -0
  319. package/src/test/nativeModule.js +33 -0
  320. package/src/test/renderPageScreen.js +109 -0
  321. package/src/types/checkTypeSet.js +39 -0
  322. package/src/types/collectPageTypes.js +49 -0
  323. package/src/types/collectPageTypes.test.js +90 -0
  324. package/src/types/findMissingPageTypes.js +40 -0
  325. package/src/version/compareVersions.js +38 -0
  326. package/src/version/isShellTooOld.js +30 -0
  327. package/src/version/shellVersion.js +21 -0
  328. package/src/version/shellVersion.test.js +32 -0
  329. package/src/widgets/fetchWidgetData.js +59 -0
  330. package/src/widgets/index.js +156 -0
  331. package/src/widgets/refreshWidgets.js +31 -0
  332. package/withExcludedNativeModules.js +93 -0
  333. package/dist/location.js +0 -33
  334. package/dist/public/apple-touch-icon.png +0 -0
  335. package/dist/public/icon-32.png +0 -0
  336. package/dist/public/icon-512.png +0 -0
  337. package/dist/public/icon.svg +0 -17
  338. package/dist/public/logo-dark-theme.png +0 -0
  339. package/dist/public/logo-light-theme.png +0 -0
  340. package/dist/public/logo-square-dark-theme.png +0 -0
  341. package/dist/public/logo-square-light-theme.png +0 -0
  342. package/dist/public/manifest.webmanifest +0 -16
  343. package/dist/shell/246.93432ff68186aae6c87b.js +0 -2
  344. package/dist/shell/246.93432ff68186aae6c87b.js.LICENSE.txt +0 -8
  345. package/dist/shell/454.b597ca21366afda000af.js +0 -2
  346. package/dist/shell/454.b597ca21366afda000af.js.LICENSE.txt +0 -14
  347. package/dist/shell/484.d645e6c09079f45ce044.js +0 -1
  348. package/dist/shell/706.a66931e02a549fd269bd.js +0 -2
  349. package/dist/shell/706.a66931e02a549fd269bd.js.LICENSE.txt +0 -8
  350. package/dist/shell/9.78be8c1b6c3986f404fc.js +0 -2
  351. package/dist/shell/9.78be8c1b6c3986f404fc.js.LICENSE.txt +0 -45
  352. package/dist/shell/922.43b7a3ebe22ef05b3049.js +0 -2
  353. package/dist/shell/922.43b7a3ebe22ef05b3049.js.LICENSE.txt +0 -14
  354. package/dist/shell/index.html +0 -57
  355. package/dist/shell/main.f01501a75909f38ccc32.js +0 -1
  356. package/dist/shell/runtime.e50f6c3a5f7c74a0a717.js +0 -1
@@ -0,0 +1,492 @@
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
+ import { act, fireEvent, screen, waitFor } from '@testing-library/react-native';
19
+ import { StyleSheet } from 'react-native';
20
+ import dayjs from 'dayjs';
21
+
22
+ import { createRoot } from '../test/fixtures.js';
23
+ import helloJsEntries from '../../fixtures/hello.jsEntries.json';
24
+ import reloadApp from '../dev/tab/reloadApp.js';
25
+ import renderPageScreen from '../test/renderPageScreen.js';
26
+
27
+ const mockNavigation = { setOptions: jest.fn() };
28
+ // Whether the screen is the focused one; a screen under another is not.
29
+ const mockFocus = { focused: true };
30
+
31
+ // The native stack's header height, which a Screen's headerBottom pins under on iOS.
32
+ jest.mock('expo-router/native-stack', () => ({ useAnimatedHeaderHeight: () => 0 }));
33
+ jest.mock('react-native-safe-area-context', () => ({
34
+ useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 34, left: 0 }),
35
+ }));
36
+
37
+ jest.mock('expo-router', () => ({
38
+ DarkTheme: { fonts: {} },
39
+ DefaultTheme: { fonts: {} },
40
+ ThemeProvider: ({ children }) => children,
41
+ router: { back: jest.fn(), canGoBack: () => false },
42
+ useFocusEffect: (effect) => {
43
+ const { useEffect: useReactEffect } = jest.requireActual('react');
44
+ useReactEffect(() => (mockFocus.focused ? effect() : undefined), [effect]);
45
+ },
46
+ useIsFocused: () => mockFocus.focused,
47
+ useNavigation: () => mockNavigation,
48
+ useRoute: () => ({ key: 'route-1' }),
49
+ }));
50
+
51
+ const boxTypes = {
52
+ blocks: [
53
+ { type: 'Box', package: '@lowdefy/blocks-native', version: 7 },
54
+ { type: 'Screen', package: '@lowdefy/blocks-native', version: 7 },
55
+ ],
56
+ };
57
+
58
+ beforeEach(() => {
59
+ mockFocus.focused = true;
60
+ });
61
+
62
+ // A Screen page of Box blocks from the native block set.
63
+ function screenPage(pageId, title, blocks) {
64
+ return {
65
+ id: `page:${pageId}`,
66
+ pageId,
67
+ blockId: pageId,
68
+ type: 'Screen',
69
+ properties: { title },
70
+ slots: {
71
+ content: {
72
+ blocks: (
73
+ blocks ?? [
74
+ { blockId: 'greeting', type: 'Box', properties: { content: `Hello from ${pageId}` } },
75
+ ]
76
+ ).map((block, index) => ({ id: `block:${pageId}:${block.blockId}:${index}`, ...block })),
77
+ },
78
+ },
79
+ };
80
+ }
81
+
82
+ function renderPath(path, rootOverrides = {}) {
83
+ return renderPageScreen({
84
+ path,
85
+ pages: {
86
+ today: { pageConfig: screenPage('today', 'Today'), etag: '"p1"' },
87
+ map: { pageConfig: screenPage('map', 'Map'), etag: '"p2"' },
88
+ },
89
+ root: createRoot({
90
+ pageIndex: [
91
+ { path: 'today', pageId: 'today', hash: 'h1', typesKey: 'screen' },
92
+ { path: 'map', pageId: 'map', hash: 'h2', typesKey: 'newBlock' },
93
+ ],
94
+ typeSets: {
95
+ screen: boxTypes,
96
+ newBlock: { blocks: [{ type: 'Map', package: '@lowdefy/blocks-native', version: 7 }] },
97
+ },
98
+ ...rootOverrides,
99
+ }),
100
+ });
101
+ }
102
+
103
+ // One Screen page at "today", with the root's other fields as given.
104
+ function renderTodayPage({ blocks, deviceLocales, page = {}, root = {} }) {
105
+ return renderPageScreen({
106
+ deviceLocales,
107
+ path: 'today',
108
+ pages: {
109
+ today: { pageConfig: { ...screenPage('today', 'Today', blocks), ...page }, etag: '"p1"' },
110
+ },
111
+ root: createRoot({
112
+ pageIndex: [{ path: 'today', pageId: 'today', hash: 'h1', typesKey: 'screen' }],
113
+ typeSets: { screen: boxTypes },
114
+ ...root,
115
+ }),
116
+ });
117
+ }
118
+
119
+ test('a cached Screen page renders and reports its title to the stack header', async () => {
120
+ const { session } = await renderPath('today');
121
+ expect(await screen.findByText('Hello from today')).toBeTruthy();
122
+ await waitFor(() =>
123
+ expect(mockNavigation.setOptions).toHaveBeenCalledWith(
124
+ expect.objectContaining({
125
+ headerLargeTitleEnabled: false,
126
+ headerSearchBarOptions: undefined,
127
+ presentation: 'card',
128
+ title: 'Today',
129
+ })
130
+ )
131
+ );
132
+ expect(session.getCurrentPath()).toBe('/today');
133
+ });
134
+
135
+ test('a Screen with a large title sets it on the bar a frame before the page is first drawn', async () => {
136
+ // iOS keeps a large title collapsed when the page's scroll view is laid out first under a
137
+ // standard bar: the page waits a frame for the bar to take the large title.
138
+ const frames = [];
139
+ const requestFrame = jest
140
+ .spyOn(global, 'requestAnimationFrame')
141
+ .mockImplementation((callback) => frames.push(callback));
142
+ try {
143
+ await renderTodayPage({ page: { properties: { title: 'Today', largeTitle: true } } });
144
+ await waitFor(() =>
145
+ expect(mockNavigation.setOptions).toHaveBeenCalledWith(
146
+ expect.objectContaining({ headerLargeTitleEnabled: true, title: 'Today' })
147
+ )
148
+ );
149
+ expect(screen.queryByText('Hello from today')).toBeNull();
150
+ await act(async () => {
151
+ frames.forEach((callback) => callback(0));
152
+ });
153
+ expect(await screen.findByText('Hello from today')).toBeTruthy();
154
+ } finally {
155
+ requestFrame.mockRestore();
156
+ }
157
+ });
158
+
159
+ test('a page needing a type the shell lacks shows the update screen', async () => {
160
+ await renderPath('map');
161
+ expect(await screen.findByText('Update the app to open this page')).toBeTruthy();
162
+ });
163
+
164
+ test('every page shows the update screen below minShellVersion', async () => {
165
+ await renderPath('today', { minShellVersion: '99.0.0' });
166
+ expect(await screen.findByTestId('shell-update')).toBeTruthy();
167
+ });
168
+
169
+ test('a path with no mobile page that the server does not have is not found', async () => {
170
+ await renderPath('nowhere');
171
+ expect(await screen.findByText('Page not found')).toBeTruthy();
172
+ });
173
+
174
+ function onClick(actions) {
175
+ return { onClick: { try: actions, catch: [] } };
176
+ }
177
+
178
+ test('the app events and a page run their _js from the module text the build writes', async () => {
179
+ await renderTodayPage({
180
+ blocks: [
181
+ {
182
+ blockId: 'greeting',
183
+ type: 'Box',
184
+ properties: { content: { _js: 'HorPchxU3cTNYhB6dbx9DFcepx4=' } },
185
+ },
186
+ {
187
+ blockId: 'year',
188
+ type: 'Box',
189
+ properties: { content: { _js: 'b+IYcPY3kW9J7YdZ4RyICtelfz8=' } },
190
+ },
191
+ ],
192
+ page: { _jsEntries: helloJsEntries.today },
193
+ root: {
194
+ _jsEntries: helloJsEntries.app,
195
+ events: {
196
+ onInit: {
197
+ try: [
198
+ {
199
+ id: 'set_greeting',
200
+ type: 'SetGlobal',
201
+ params: { greeting: { _js: 'yHZGzdxAuSQ8P5USi0AFFY81iNg=' } },
202
+ },
203
+ ],
204
+ catch: [],
205
+ },
206
+ },
207
+ },
208
+ });
209
+ expect(await screen.findByText('Hello from Lowdefy')).toBeTruthy();
210
+ expect(screen.getByText(String(new Date().getFullYear()))).toBeTruthy();
211
+ });
212
+
213
+ test('pages take their style from the root theme and class map, and a dev page its own classes', async () => {
214
+ const { host } = await renderTodayPage({
215
+ blocks: [
216
+ {
217
+ blockId: 'panel',
218
+ type: 'Box',
219
+ class: { block: 'p-4 dark:bg-gray-900' },
220
+ properties: { content: 'Panel' },
221
+ },
222
+ { blockId: 'big', type: 'Box', class: { block: 'text-xl' }, properties: { content: 'Big' } },
223
+ ],
224
+ page: { _classMap: { 'text-xl': { fontSize: 20 } } },
225
+ root: {
226
+ theme: { darkMode: 'dark', light: {}, dark: { colorPrimary: '#1668dc' } },
227
+ classMap: {
228
+ 'p-4': { paddingTop: 16 },
229
+ 'bg-gray-100': { backgroundColor: '#f3f4f6' },
230
+ 'dark:bg-gray-900': { backgroundColor: '#101828' },
231
+ },
232
+ },
233
+ });
234
+ await screen.findByText('Panel');
235
+ expect(StyleSheet.flatten(screen.getByTestId('panel').props.style)).toMatchObject({
236
+ backgroundColor: '#101828',
237
+ paddingTop: 16,
238
+ });
239
+ expect(StyleSheet.flatten(screen.getByTestId('big').props.style)).toMatchObject({ fontSize: 20 });
240
+ expect(host.globals.window.__lowdefy_isDark).toBe(true);
241
+ });
242
+
243
+ const i18n = {
244
+ defaultLocale: 'en-US',
245
+ locales: [
246
+ { code: 'en-US', dayjs: 'en' },
247
+ { code: 'af-ZA', dayjs: 'af' },
248
+ ],
249
+ messages: { 'en-US': { hello: 'Hello' }, 'af-ZA': { hello: 'Hallo' } },
250
+ };
251
+
252
+ test('a page translates with the root i18n in the device locale the app supports', async () => {
253
+ await renderTodayPage({
254
+ blocks: [{ blockId: 'greeting', type: 'Box', properties: { content: { _t: 'hello' } } }],
255
+ deviceLocales: ['af-NA'],
256
+ root: { i18n },
257
+ });
258
+ expect(await screen.findByText('Hallo')).toBeTruthy();
259
+ expect(dayjs.locale()).toBe('af');
260
+ });
261
+
262
+ test('SetLocale changes the locale the page translates with, and the device keeps it', async () => {
263
+ const { storage } = await renderTodayPage({
264
+ blocks: [
265
+ { blockId: 'greeting', type: 'Box', properties: { content: { _t: 'hello' } } },
266
+ {
267
+ blockId: 'switch',
268
+ type: 'Box',
269
+ properties: { content: 'Afrikaans' },
270
+ events: onClick([{ id: 'af', type: 'SetLocale', params: { locale: 'af-ZA' } }]),
271
+ },
272
+ ],
273
+ root: { i18n },
274
+ });
275
+ expect(await screen.findByText('Hello')).toBeTruthy();
276
+ fireEvent.press(screen.getByText('Afrikaans'));
277
+ expect(await screen.findByText('Hallo')).toBeTruthy();
278
+ expect(storage.get('lowdefy:field::lowdefy_locale')).toBe('af-ZA');
279
+ });
280
+
281
+ test('the local storage actions keep a value under the app key in the device store', async () => {
282
+ const { storage } = await renderTodayPage({
283
+ blocks: [
284
+ {
285
+ blockId: 'saved',
286
+ type: 'Box',
287
+ properties: { content: { _if_none: [{ _state: 'draft.title' }, 'none'] } },
288
+ },
289
+ {
290
+ blockId: 'save',
291
+ type: 'Box',
292
+ properties: { content: 'Save' },
293
+ events: onClick([
294
+ {
295
+ id: 'set',
296
+ type: 'SetLocalStorage',
297
+ params: { key: 'draft', value: { title: 'Pump' } },
298
+ },
299
+ { id: 'get', type: 'GetLocalStorage', params: { key: 'draft' } },
300
+ { id: 'state', type: 'SetState', params: { draft: { _actions: 'get.response' } } },
301
+ ]),
302
+ },
303
+ ],
304
+ });
305
+ expect(await screen.findByText('none')).toBeTruthy();
306
+ fireEvent.press(screen.getByText('Save'));
307
+ expect(await screen.findByText('Pump')).toBeTruthy();
308
+ expect(storage.get('lowdefy:field::draft')).toBe('{"title":"Pump"}');
309
+ });
310
+
311
+ test('CopyToClipboard copies through the shell clipboard', async () => {
312
+ const { clipboard } = await renderTodayPage({
313
+ blocks: [
314
+ {
315
+ blockId: 'copy',
316
+ type: 'Box',
317
+ properties: { content: 'Copy' },
318
+ events: onClick([{ id: 'copy', type: 'CopyToClipboard', params: { copy: 'JOB-42' } }]),
319
+ },
320
+ ],
321
+ });
322
+ fireEvent.press(await screen.findByText('Copy'));
323
+ await waitFor(() => expect(clipboard.setString.mock.calls).toEqual([['JOB-42']]));
324
+ });
325
+
326
+ test('ListOrganizations and SetActiveOrganization call the app server over the bearer client', async () => {
327
+ const { server, session } = await renderTodayPage({
328
+ blocks: [
329
+ {
330
+ blockId: 'orgs',
331
+ type: 'Box',
332
+ properties: { content: { _if_none: [{ _state: 'org' }, 'none'] } },
333
+ },
334
+ {
335
+ blockId: 'list',
336
+ type: 'Box',
337
+ properties: { content: 'List' },
338
+ events: onClick([
339
+ { id: 'memberships', type: 'ListOrganizations' },
340
+ {
341
+ id: 'state',
342
+ type: 'SetState',
343
+ params: { org: { _actions: 'memberships.response.1.name' } },
344
+ },
345
+ ]),
346
+ },
347
+ {
348
+ blockId: 'switch',
349
+ type: 'Box',
350
+ properties: { content: 'Switch' },
351
+ events: onClick([
352
+ { id: 'activate', type: 'SetActiveOrganization', params: { organizationId: 'org-2' } },
353
+ { id: 'resync', type: 'UpdateSession' },
354
+ ]),
355
+ },
356
+ ],
357
+ });
358
+ server.state.auth = ({ path }) => {
359
+ if (path === '/organization/list') {
360
+ return {
361
+ body: [
362
+ { id: 'org-1', name: 'Acme' },
363
+ { id: 'org-2', name: 'Globex' },
364
+ ],
365
+ };
366
+ }
367
+ if (path === '/organization/set-active') {
368
+ server.state.user = { id: 'ana', organization_id: 'org-2', roles: [] };
369
+ return { body: { id: 'org-2', name: 'Globex' } };
370
+ }
371
+ return { body: { session: { id: 's1' }, user: { id: 'ana' } } };
372
+ };
373
+ fireEvent.press(await screen.findByText('List'));
374
+ expect(await screen.findByText('Globex')).toBeTruthy();
375
+ fireEvent.press(screen.getByText('Switch'));
376
+ await waitFor(() => expect(session.getState().user?.organization_id).toBe('org-2'));
377
+ const authPaths = server.fetch.mock.calls
378
+ .map(([url]) => new URL(url).pathname)
379
+ .filter((pathname) => pathname.startsWith('/api/auth/'));
380
+ expect(authPaths).toEqual([
381
+ '/api/auth/organization/list',
382
+ '/api/auth/organization/set-active',
383
+ '/api/auth/get-session',
384
+ ]);
385
+ });
386
+
387
+ function findAll(node, predicate, found = []) {
388
+ if (node === null || typeof node === 'string') return found;
389
+ if (predicate(node)) found.push(node);
390
+ (node.children ?? []).forEach((child) => findAll(child, predicate, found));
391
+ return found;
392
+ }
393
+
394
+ test('an Icon block draws the icon data the page carries', async () => {
395
+ const housePath = 'M3 10a2 2 0 0 1 .709-1.528l7-6';
396
+ await renderTodayPage({
397
+ blocks: [{ blockId: 'home', type: 'Icon', properties: { name: 'House' } }],
398
+ page: { _icons: { House: { node: [['path', { d: housePath }]] } } },
399
+ });
400
+ const icon = await screen.findByLabelText('House');
401
+ expect(findAll(icon, (node) => node.props?.d === housePath)).not.toHaveLength(0);
402
+ });
403
+
404
+ test('a screen reads its own route query, not the focused screen route', async () => {
405
+ // Another screen (pushed on this one) is focused: the shell's shown route is its route.
406
+ mockFocus.focused = false;
407
+ await renderPageScreen({
408
+ beforeRender: ({ session }) => session.setCurrentPath('/today?key=ACME-1', 'route-top'),
409
+ path: 'today',
410
+ query: 'space=ACME&key=ACME-645',
411
+ pages: {
412
+ today: {
413
+ pageConfig: screenPage('today', 'Today', [
414
+ {
415
+ blockId: 'key',
416
+ type: 'Box',
417
+ properties: { content: { '_string.concat': ['key ', { _url_query: 'key' }] } },
418
+ },
419
+ ]),
420
+ etag: '"p1"',
421
+ },
422
+ },
423
+ root: createRoot({
424
+ pageIndex: [{ path: 'today', pageId: 'today', hash: 'h1', typesKey: 'screen' }],
425
+ typeSets: { screen: boxTypes },
426
+ }),
427
+ });
428
+ expect(await screen.findByText('key ACME-645')).toBeTruthy();
429
+ });
430
+
431
+ test('a changed page the shell fetches replaces the version the screen shows', async () => {
432
+ // A dev root: pages carry no hash, and each visit fetches the page again.
433
+ const { server, session } = await renderPageScreen({
434
+ path: 'today',
435
+ pages: {
436
+ today: {
437
+ pageConfig: screenPage('today', 'Today', [
438
+ { blockId: 'greeting', type: 'Box', properties: { content: 'Version 1' } },
439
+ ]),
440
+ etag: '"v1"',
441
+ },
442
+ },
443
+ root: createRoot({
444
+ pageIndex: [{ path: 'today', pageId: 'today', typesKey: 'screen' }],
445
+ typeSets: { screen: boxTypes },
446
+ }),
447
+ });
448
+ expect(await screen.findByText('Version 1')).toBeTruthy();
449
+ server.state.pages.today = {
450
+ pageConfig: screenPage('today', 'Today', [
451
+ { blockId: 'greeting', type: 'Box', properties: { content: 'Version 2' } },
452
+ ]),
453
+ etag: '"v2"',
454
+ };
455
+ // The dev server rebuilt the app: the shell fetches the page on screen and rebuilds its screens.
456
+ await act(async () => {
457
+ await reloadApp({ session });
458
+ });
459
+ expect(await screen.findByText('Version 2')).toBeTruthy();
460
+ expect(screen.queryByText('Version 1')).toBeNull();
461
+ });
462
+
463
+ test('a newer version fetched while a screen shows its cached one is drawn on that screen', async () => {
464
+ const { server, session } = await renderPageScreen({
465
+ path: 'today',
466
+ pages: {
467
+ today: {
468
+ pageConfig: screenPage('today', 'Today', [
469
+ { blockId: 'greeting', type: 'Box', properties: { content: 'Cached' } },
470
+ ]),
471
+ etag: '"v1"',
472
+ },
473
+ },
474
+ root: createRoot({
475
+ pageIndex: [{ path: 'today', pageId: 'today', typesKey: 'screen' }],
476
+ typeSets: { screen: boxTypes },
477
+ }),
478
+ });
479
+ expect(await screen.findByText('Cached')).toBeTruthy();
480
+ server.state.pages.today = {
481
+ pageConfig: screenPage('today', 'Today', [
482
+ { blockId: 'greeting', type: 'Box', properties: { content: 'Deployed' } },
483
+ ]),
484
+ etag: '"v2"',
485
+ };
486
+ const entry = session.getState().root.pageIndex[0];
487
+ await act(async () => {
488
+ await session.loadPage({ entry, path: 'today' });
489
+ });
490
+ expect(await screen.findByText('Deployed')).toBeTruthy();
491
+ expect(screen.queryByText('Cached')).toBeNull();
492
+ });
@@ -0,0 +1,47 @@
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 } from 'react';
18
+ import * as SplashScreen from 'expo-splash-screen';
19
+
20
+ import MessageScreen from './MessageScreen.js';
21
+ import reportShellError from '../host/reportShellError.js';
22
+
23
+ /**
24
+ * The root layout's Expo Router ErrorBoundary: a render fault outside a block (a page's `_js`, the
25
+ * root config, the frame) would otherwise close the app unreported. It reports the fault and offers
26
+ * to render the screen again. A block's own fault never reaches it: the block's boundary shows it.
27
+ *
28
+ * @param {Object} props
29
+ * @param {Error} props.error
30
+ * @param {() => Promise<void>} props.retry Renders the route again.
31
+ */
32
+ function RouteErrorScreen({ error, retry }) {
33
+ useEffect(() => {
34
+ reportShellError({ error });
35
+ SplashScreen.hideAsync();
36
+ }, [error]);
37
+ return (
38
+ <MessageScreen
39
+ action={{ title: 'Try again', onPress: () => retry() }}
40
+ body={error?.message}
41
+ testID="shell-route-error"
42
+ title="Something went wrong"
43
+ />
44
+ );
45
+ }
46
+
47
+ export default RouteErrorScreen;
@@ -0,0 +1,34 @@
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, { useMemo } from 'react';
18
+
19
+ import AppProvider from './AppProvider.js';
20
+ import Frame from './Frame.js';
21
+ import getShellConfig from '../config/getShellConfig.js';
22
+
23
+ // The shell opens its one app (shell.json `app`). A shell never opens another app: every app's `_js`
24
+ // runs in this one JavaScript realm, beside the app's session token, storage and cache.
25
+ function ShellRoot() {
26
+ const shell = useMemo(() => getShellConfig(), []);
27
+ return (
28
+ <AppProvider shell={shell}>
29
+ <Frame />
30
+ </AppProvider>
31
+ );
32
+ }
33
+
34
+ export default ShellRoot;
@@ -0,0 +1,62 @@
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, screen } from '@testing-library/react-native';
20
+
21
+ import ShellRoot from './ShellRoot.js';
22
+
23
+ const mockShell = {
24
+ id: 'field',
25
+ name: 'Field',
26
+ environment: 'production',
27
+ app: { id: 'field', name: 'Field', urls: { production: 'https://field.example.com' } },
28
+ };
29
+
30
+ // The opened app is recorded, not built: its auth, session and screens have their own tests.
31
+ const mockOpened = [];
32
+ jest.mock('./AppProvider.js', () => {
33
+ function AppProvider({ children, shell }) {
34
+ mockOpened.push(shell);
35
+ return children;
36
+ }
37
+ return AppProvider;
38
+ });
39
+ jest.mock('./Frame.js', () => {
40
+ const { Text: MockText } = jest.requireActual('react-native');
41
+ return () => <MockText testID="frame">frame</MockText>;
42
+ });
43
+ jest.mock('../config/getShellConfig.js', () => () => mockShell);
44
+ // No app is remembered between launches: there is only one to open.
45
+ jest.mock('../host/createDeviceStore.js', () => () => {
46
+ throw new Error('ShellRoot reads no device store.');
47
+ });
48
+
49
+ beforeEach(() => {
50
+ mockOpened.length = 0;
51
+ });
52
+
53
+ test('the shell opens its one app at once, with no app picker', async () => {
54
+ await render(<ShellRoot />);
55
+ expect(screen.getByTestId('frame')).toBeTruthy();
56
+ expect(mockOpened.length).toBeGreaterThan(0);
57
+ mockOpened.forEach((shell) => {
58
+ expect(shell).toBe(mockShell);
59
+ expect(shell.app.id).toBe('field');
60
+ });
61
+ expect(screen.queryByTestId('app-field')).toBeNull();
62
+ });
@@ -0,0 +1,45 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import { ThemeProvider } from '@lowdefy/client-native';
19
+ import { appStorageKey } from '@lowdefy/helpers';
20
+
21
+ import NavigationTheme from './NavigationTheme.js';
22
+ import { useSessionState, useShell } from '../ShellContext.js';
23
+
24
+ // One theme for every screen of the app, so all its pages show the same mode: the mobile root's
25
+ // theme, and the dark mode preference SetDarkMode keeps under the app's key, as on the web. The
26
+ // navigators draw their headers, tab bar and screens from it too.
27
+ function ShellThemeProvider({ children }) {
28
+ const { host } = useShell();
29
+ const { rootConfig } = useSessionState();
30
+ return (
31
+ <ThemeProvider
32
+ host={host}
33
+ storageKey={appStorageKey({
34
+ basePath: '',
35
+ name: 'lowdefy_darkMode',
36
+ slug: rootConfig.lowdefyApp.slug,
37
+ })}
38
+ theme={rootConfig.theme}
39
+ >
40
+ <NavigationTheme>{children}</NavigationTheme>
41
+ </ThemeProvider>
42
+ );
43
+ }
44
+
45
+ export default ShellThemeProvider;