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