@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.
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.bde87d1433f0625faed6.js +0 -1
  356. package/dist/shell/runtime.1ead93159ba56c05b969.js +0 -1
@@ -0,0 +1,115 @@
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, screen } from '@testing-library/react-native';
19
+
20
+ import getShownPage from './getShownPage.js';
21
+ import reloadApp from './reloadApp.js';
22
+ import renderPageScreen from '../../test/renderPageScreen.js';
23
+ import { createRoot } from '../../test/fixtures.js';
24
+
25
+ // The native stack's header height, which a Screen's headerBottom pins under on iOS.
26
+ jest.mock('expo-router/native-stack', () => ({ useAnimatedHeaderHeight: () => 0 }));
27
+ jest.mock('react-native-safe-area-context', () => ({
28
+ useSafeAreaInsets: () => ({ top: 0, right: 0, bottom: 34, left: 0 }),
29
+ }));
30
+
31
+ jest.mock('expo-router', () => ({
32
+ DarkTheme: { fonts: {} },
33
+ DefaultTheme: { fonts: {} },
34
+ ThemeProvider: ({ children }) => children,
35
+ router: { back: jest.fn(), canGoBack: () => false },
36
+ useFocusEffect: (effect) => {
37
+ const { useEffect: useReactEffect } = jest.requireActual('react');
38
+ useReactEffect(effect, [effect]);
39
+ },
40
+ useIsFocused: () => true,
41
+ useNavigation: () => ({ setOptions: jest.fn() }),
42
+ useRoute: () => ({ key: 'route-1' }),
43
+ }));
44
+
45
+ function todayPage(greeting) {
46
+ return {
47
+ id: 'page:today',
48
+ pageId: 'today',
49
+ blockId: 'today',
50
+ type: 'Screen',
51
+ properties: { title: 'Today' },
52
+ slots: {
53
+ content: {
54
+ blocks: [
55
+ {
56
+ id: 'block:today:greeting:0',
57
+ blockId: 'greeting',
58
+ type: 'Box',
59
+ properties: { content: greeting },
60
+ },
61
+ ],
62
+ },
63
+ },
64
+ };
65
+ }
66
+
67
+ // A dev root: its pages have no hash, so the shell fetches them on each visit.
68
+ async function renderDevPage() {
69
+ const { server, session } = await renderPageScreen({
70
+ path: 'today',
71
+ root: createRoot({
72
+ pageIndex: [{ path: 'today', pageId: 'today', hash: null, typesKey: 'screen' }],
73
+ typeSets: {
74
+ screen: {
75
+ blocks: [
76
+ { type: 'Box', package: '@lowdefy/blocks-native', version: 7 },
77
+ { type: 'Screen', package: '@lowdefy/blocks-native', version: 7 },
78
+ ],
79
+ },
80
+ },
81
+ }),
82
+ pages: { today: { pageConfig: todayPage('Hello'), etag: '"p1"' } },
83
+ });
84
+ await screen.findByText('Hello');
85
+ return { server, session };
86
+ }
87
+
88
+ test('a reload refetches the root and the page on screen, and rebuilds it with empty state', async () => {
89
+ const { server, session } = await renderDevPage();
90
+ const before = session.getState().lowdefy;
91
+ const shown = getShownPage(before);
92
+ // The screen's page context is keyed by its route instance.
93
+ expect(shown).toEqual({ pageId: 'today', pathParams: {}, instanceKey: 'page:today@route-1' });
94
+ before.contexts[shown.instanceKey]._internal.State.set('typed', 'Quarterly');
95
+ expect(before.contexts[shown.instanceKey].state.typed).toBe('Quarterly');
96
+
97
+ server.state.pages.today = { pageConfig: todayPage('Hello again'), etag: '"p2"' };
98
+ server.state.rootEtag = '"r2"';
99
+ server.fetch.mockClear();
100
+ await act(async () => {
101
+ await reloadApp({ session });
102
+ });
103
+
104
+ const paths = server.fetch.mock.calls.map(([url]) => new URL(url).pathname);
105
+ // The rebuilt screen revalidates its page, as every screen does after a rebuild, so a screen
106
+ // under the one shown gets its new version too: the version reloadApp fetched is current.
107
+ expect(paths).toEqual(['/api/page/today', '/api/mobile/root', '/api/user', '/api/page/today']);
108
+ const revalidation = server.fetch.mock.calls.at(-1)[1];
109
+ expect(revalidation.headers).toEqual({ 'If-None-Match': '"p2"' });
110
+ expect(await screen.findByText('Hello again')).toBeTruthy();
111
+ expect(session.getCurrentPath()).toBe('/today');
112
+ const after = session.getState().lowdefy;
113
+ expect(after).not.toBe(before);
114
+ expect(after.contexts[shown.instanceKey].state.typed).toBeUndefined();
115
+ });
@@ -0,0 +1,32 @@
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
+
19
+ import device from './device.js';
20
+
21
+ // The views device features open over the app (the ScanCode sheet), mounted once at the root.
22
+ function DeviceHosts() {
23
+ return (
24
+ <>
25
+ {device.hosts.map((Host) => (
26
+ <Host key={Host.name} />
27
+ ))}
28
+ </>
29
+ );
30
+ }
31
+
32
+ export default DeviceHosts;
@@ -0,0 +1,41 @@
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 DocumentPicker from 'expo-document-picker';
18
+
19
+ import toPickedFile from './toPickedFile.js';
20
+
21
+ /**
22
+ * The files device feature: picks files from Files (iOS) or the document picker (Android), copied
23
+ * into the app's cache so they can be read and uploaded. [] when cancelled.
24
+ */
25
+ function createFilesFeature() {
26
+ return {
27
+ async pick({ base64 = false, multiple = false, types }) {
28
+ const result = await DocumentPicker.getDocumentAsync({
29
+ copyToCacheDirectory: true,
30
+ multiple,
31
+ type: types ?? '*/*',
32
+ });
33
+ if (result.canceled) {
34
+ return [];
35
+ }
36
+ return Promise.all(result.assets.map((asset) => toPickedFile({ asset, base64 })));
37
+ },
38
+ };
39
+ }
40
+
41
+ export default createFilesFeature;
@@ -0,0 +1,46 @@
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 Haptics from 'expo-haptics';
18
+
19
+ const IMPACTS = {
20
+ light: Haptics.ImpactFeedbackStyle.Light,
21
+ medium: Haptics.ImpactFeedbackStyle.Medium,
22
+ heavy: Haptics.ImpactFeedbackStyle.Heavy,
23
+ };
24
+ const NOTIFICATIONS = {
25
+ success: Haptics.NotificationFeedbackType.Success,
26
+ warning: Haptics.NotificationFeedbackType.Warning,
27
+ error: Haptics.NotificationFeedbackType.Error,
28
+ };
29
+
30
+ /**
31
+ * The haptics device feature: `(type) => Promise`, a selection tick, an impact or a notification
32
+ * (blocks-native's Haptic action and the native controls' feedback).
33
+ */
34
+ function createHapticsFeature() {
35
+ return function playHaptic(hapticType) {
36
+ if (hapticType === 'selection') {
37
+ return Haptics.selectionAsync();
38
+ }
39
+ if (NOTIFICATIONS[hapticType] !== undefined) {
40
+ return Haptics.notificationAsync(NOTIFICATIONS[hapticType]);
41
+ }
42
+ return Haptics.impactAsync(IMPACTS[hapticType] ?? IMPACTS.light);
43
+ };
44
+ }
45
+
46
+ export default createHapticsFeature;
@@ -0,0 +1,37 @@
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 { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
18
+
19
+ const TAG = 'lowdefy-keep-awake';
20
+
21
+ /**
22
+ * The keepAwake device feature (KeepAwake): keeps the screen on, or lets it sleep again. The shell
23
+ * lets it sleep when the app goes to the background.
24
+ */
25
+ function createKeepAwakeFeature() {
26
+ return {
27
+ async set({ on = true }) {
28
+ if (on) {
29
+ await activateKeepAwakeAsync(TAG);
30
+ return;
31
+ }
32
+ deactivateKeepAwake(TAG);
33
+ },
34
+ };
35
+ }
36
+
37
+ export default createKeepAwakeFeature;
@@ -0,0 +1,40 @@
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 ImagePicker from 'expo-image-picker';
18
+
19
+ import toImagePickerOptions from './toImagePickerOptions.js';
20
+ import toPickedFile from './toPickedFile.js';
21
+
22
+ /**
23
+ * The photos device feature: picks photos and videos with the system picker, which needs no
24
+ * permission (PHPicker on iOS, the Photo Picker on Android). [] when cancelled.
25
+ */
26
+ function createPhotosFeature() {
27
+ return {
28
+ async pick(params) {
29
+ const result = await ImagePicker.launchImageLibraryAsync(toImagePickerOptions(params));
30
+ if (result.canceled) {
31
+ return [];
32
+ }
33
+ return Promise.all(
34
+ result.assets.map((asset) => toPickedFile({ asset, base64: params.base64 === true }))
35
+ );
36
+ },
37
+ };
38
+ }
39
+
40
+ export default createPhotosFeature;
@@ -0,0 +1,37 @@
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 { getCurrentApp } from '../../native/currentApp.js';
18
+
19
+ /**
20
+ * The requestQueue device feature (GetRequestQueue, RetryRequestQueue, DiscardQueuedRequest): the
21
+ * open app's offline request queue (src/queue/createRequestQueue.js).
22
+ */
23
+ function createRequestQueueFeature() {
24
+ return {
25
+ async get() {
26
+ return (await getCurrentApp()).requestQueue.get();
27
+ },
28
+ async retry(params) {
29
+ return (await getCurrentApp()).requestQueue.retry(params);
30
+ },
31
+ async discard(params) {
32
+ return (await getCurrentApp()).requestQueue.discard(params);
33
+ },
34
+ };
35
+ }
36
+
37
+ export default createRequestQueueFeature;
@@ -0,0 +1,63 @@
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 { Share } from 'react-native';
18
+ import { File, Paths } from 'expo-file-system';
19
+ import * as Sharing from 'expo-sharing';
20
+ import { type } from '@lowdefy/helpers';
21
+
22
+ // The last segment of a name from page data, so "../" cannot write outside the cache.
23
+ function toFileName(name) {
24
+ const base = String(name ?? '')
25
+ .split(/[\\/]/)
26
+ .pop();
27
+ return base === '' || base === '.' || base === '..' ? 'shared-file' : base;
28
+ }
29
+
30
+ // The file to share: a file on the phone as it is, an https file downloaded to the cache first, or
31
+ // base64 content written to the cache under its name.
32
+ async function toLocalFile({ base64, name, uri }) {
33
+ if (type.isString(base64)) {
34
+ const file = new File(Paths.cache, toFileName(name));
35
+ if (file.exists) file.delete();
36
+ file.create();
37
+ file.write(base64, { encoding: 'base64' });
38
+ return file.uri;
39
+ }
40
+ if (/^https?:\/\//.test(uri)) {
41
+ const file = await File.downloadFileAsync(uri, Paths.cache, { idempotent: true });
42
+ return file.uri;
43
+ }
44
+ return uri;
45
+ }
46
+
47
+ /**
48
+ * The share device feature: the system share sheet with text or a link (React Native's Share), or
49
+ * a file (expo-sharing). Returns { shared }; iOS tells a dismissed sheet apart, Android does not.
50
+ */
51
+ function createShareFeature() {
52
+ return async function share({ base64, mimeType, name, text, title, uri, url }) {
53
+ if (type.isString(uri) || type.isString(base64)) {
54
+ const local = await toLocalFile({ base64, name, uri });
55
+ await Sharing.shareAsync(local, { dialogTitle: title, mimeType });
56
+ return { shared: true };
57
+ }
58
+ const result = await Share.share({ message: text ?? url, title, url });
59
+ return { shared: result.action !== Share.dismissedAction };
60
+ };
61
+ }
62
+
63
+ export default createShareFeature;
@@ -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 createShareFeature from './createShareFeature.js';
18
+
19
+ // expo-file-system and expo-sharing both map to the native stand-in (jest.config.js), so one mock
20
+ // serves both.
21
+ jest.mock('expo-file-system', () => ({
22
+ Paths: { cache: 'file:///cache' },
23
+ File: class {
24
+ constructor(directory, name) {
25
+ this.uri = `${directory}/${name}`;
26
+ this.exists = false;
27
+ }
28
+ create() {}
29
+ write() {}
30
+ },
31
+ shareAsync: jest.fn(async () => undefined),
32
+ }));
33
+
34
+ const Sharing = require('expo-sharing');
35
+
36
+ async function shareNamed(name) {
37
+ await createShareFeature()({ base64: 'aGk=', name });
38
+ return Sharing.shareAsync.mock.calls.at(-1)[0];
39
+ }
40
+
41
+ test('a shared base64 file is written to the cache under the last segment of its name', async () => {
42
+ expect(await shareNamed('report.pdf')).toBe('file:///cache/report.pdf');
43
+ expect(await shareNamed('../../Documents/token.json')).toBe('file:///cache/token.json');
44
+ expect(await shareNamed('..\\secrets.txt')).toBe('file:///cache/secrets.txt');
45
+ expect(await shareNamed('..')).toBe('file:///cache/shared-file');
46
+ expect(await shareNamed(undefined)).toBe('file:///cache/shared-file');
47
+ });
@@ -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
+ import * as Speech from 'expo-speech';
18
+
19
+ /**
20
+ * The speech device feature (Speak, StopSpeaking): reads text aloud with the phone's voice. speak
21
+ * resolves when the text is done, stopped or fails.
22
+ */
23
+ function createSpeechFeature() {
24
+ return {
25
+ speak({ language, pitch, rate, text }) {
26
+ return new Promise((resolve) => {
27
+ Speech.speak(text, {
28
+ language,
29
+ pitch,
30
+ rate,
31
+ onDone: () => resolve({ done: true }),
32
+ onStopped: () => resolve({ done: false }),
33
+ onError: () => resolve({ done: false }),
34
+ });
35
+ });
36
+ },
37
+ async stop() {
38
+ await Speech.stop();
39
+ },
40
+ };
41
+ }
42
+
43
+ export default createSpeechFeature;
@@ -0,0 +1,41 @@
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 { File } from 'expo-file-system';
18
+
19
+ import callEndpoint from '../../native/callEndpoint.js';
20
+ import { getCurrentApp } from '../../native/currentApp.js';
21
+
22
+ /**
23
+ * The upload device feature: sends one picked file to an API endpoint as
24
+ * { ...payload, file: { name, mimeType, size, base64 } } and returns the endpoint's answer. The
25
+ * endpoint stores it (S3, a database, the files module) as the app decides.
26
+ */
27
+ function createUploadFeature() {
28
+ return async function upload({ endpointId, file, payload }) {
29
+ const base64 = file.base64 ?? (await new File(file.uri).base64());
30
+ return callEndpoint({
31
+ app: await getCurrentApp(),
32
+ endpointId,
33
+ payload: {
34
+ ...payload,
35
+ file: { base64, mimeType: file.mimeType, name: file.name, size: file.size },
36
+ },
37
+ });
38
+ };
39
+ }
40
+
41
+ export default createUploadFeature;
@@ -0,0 +1,41 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ const MEDIA_TYPES = { images: 'images', videos: 'videos' };
18
+
19
+ /**
20
+ * PickImage and TakePhoto params as expo-image-picker options. Videos come back as files; photos
21
+ * are compressed to `quality`.
22
+ *
23
+ * @param {Object} params
24
+ */
25
+ function toImagePickerOptions({
26
+ maxCount,
27
+ mediaTypes = ['images'],
28
+ multiple = false,
29
+ quality = 0.7,
30
+ }) {
31
+ return {
32
+ allowsMultipleSelection: multiple,
33
+ base64: false,
34
+ exif: false,
35
+ mediaTypes: mediaTypes.map((mediaType) => MEDIA_TYPES[mediaType]),
36
+ quality,
37
+ selectionLimit: multiple ? maxCount ?? 0 : 1,
38
+ };
39
+ }
40
+
41
+ export default toImagePickerOptions;
@@ -0,0 +1,54 @@
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 { File } from 'expo-file-system';
18
+
19
+ // A file name for a picked asset that has none (a camera photo on Android): its kind and the time.
20
+ function fallbackName({ mimeType }) {
21
+ const extension = (mimeType ?? 'application/octet-stream').split('/')[1] ?? 'bin';
22
+ return `file-${Date.now()}.${extension}`;
23
+ }
24
+
25
+ /**
26
+ * A picked image, video or document as the native actions return it:
27
+ * { name, mimeType, size, uri, width?, height?, duration?, base64? }.
28
+ *
29
+ * @param {Object} params
30
+ * @param {Object} params.asset An expo-image-picker or expo-document-picker asset.
31
+ * @param {boolean} [params.base64] Read the content as base64 too.
32
+ */
33
+ async function toPickedFile({ asset, base64 = false }) {
34
+ const mimeType = asset.mimeType ?? null;
35
+ const file = {
36
+ name: asset.fileName ?? asset.name ?? fallbackName({ mimeType }),
37
+ mimeType,
38
+ size: asset.fileSize ?? asset.size ?? new File(asset.uri).size ?? null,
39
+ uri: asset.uri,
40
+ };
41
+ if (typeof asset.width === 'number') {
42
+ file.width = asset.width;
43
+ file.height = asset.height;
44
+ }
45
+ if (typeof asset.duration === 'number') {
46
+ file.duration = asset.duration;
47
+ }
48
+ if (base64) {
49
+ file.base64 = asset.base64 ?? (await new File(asset.uri).base64());
50
+ }
51
+ return file;
52
+ }
53
+
54
+ export default toPickedFile;