@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,79 @@
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 { RequestError, UserError } from '@lowdefy/errors';
19
+
20
+ import createNetworkError from '../test/createNetworkError.js';
21
+ import reportShellError from './reportShellError.js';
22
+
23
+ function createSession(handleError) {
24
+ return { getState: () => ({ lowdefy: { _internal: { handleError } } }) };
25
+ }
26
+
27
+ let consoleError;
28
+
29
+ beforeEach(() => {
30
+ consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
31
+ });
32
+
33
+ afterEach(() => {
34
+ consoleError.mockRestore();
35
+ });
36
+
37
+ test('a fault goes to the core handleError once a page has made one', () => {
38
+ const handleError = jest.fn(async () => {});
39
+ const error = new RequestError('Something went wrong.');
40
+ reportShellError({ error, session: createSession(handleError) });
41
+ expect(handleError).toHaveBeenCalledWith(error);
42
+ expect(consoleError).not.toHaveBeenCalled();
43
+ });
44
+
45
+ test('a fault goes to the device console before any page made a core', () => {
46
+ const error = new Error('Push token registration failed.');
47
+ reportShellError({ error, session: { getState: () => ({ lowdefy: { buildId: 'b1' } }) } });
48
+ expect(consoleError).toHaveBeenCalledWith(error);
49
+ });
50
+
51
+ test('a fault goes to the device console when no app is open', () => {
52
+ const error = new Error('The shell could not open its database.');
53
+ reportShellError({ error });
54
+ expect(consoleError).toHaveBeenCalledWith(error);
55
+ });
56
+
57
+ test('no network and expected outcomes are not reported', () => {
58
+ const handleError = jest.fn(async () => {});
59
+ const session = createSession(handleError);
60
+ reportShellError({ error: createNetworkError(), session });
61
+ reportShellError({ error: new UserError('Camera permission was denied.'), session });
62
+ const refusal = new Error('Not allowed.');
63
+ refusal.name = 'AuthorizationError';
64
+ reportShellError({ error: refusal, session });
65
+ expect(handleError).not.toHaveBeenCalled();
66
+ expect(consoleError).not.toHaveBeenCalled();
67
+ });
68
+
69
+ test('a handleError that fails falls back to the device console', async () => {
70
+ const error = new Error('Background fetch failed.');
71
+ reportShellError({
72
+ error,
73
+ session: createSession(async () => {
74
+ throw new Error('Logger gone.');
75
+ }),
76
+ });
77
+ await new Promise((resolve) => setTimeout(resolve, 0));
78
+ expect(consoleError).toHaveBeenCalledWith(error);
79
+ });
@@ -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
+ /**
18
+ * Marks a rejection of the platform's fetch as a network failure (isNetworkError): the request never
19
+ * reached the server, so the offline queue keeps it and the screens say there is no connection.
20
+ * Marked where the platform answers, so a programming fault later in the send path (a TypeError
21
+ * reading the response) is never taken for "offline". A call the caller aborted is not marked.
22
+ *
23
+ * @param {Function} fetch The platform's fetch (React Native's, expo/fetch, or Lowdefy Dev's).
24
+ */
25
+ function withNetworkErrors(fetch) {
26
+ return async function networkErrorFetch(url, init) {
27
+ try {
28
+ return await fetch(url, init);
29
+ } catch (error) {
30
+ if (init?.signal?.aborted !== true && error instanceof Error) {
31
+ error.isNetworkError = true;
32
+ }
33
+ throw error;
34
+ }
35
+ };
36
+ }
37
+
38
+ export default withNetworkErrors;
@@ -0,0 +1,61 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { jest } from '@jest/globals';
18
+
19
+ import isNetworkError from './isNetworkError.js';
20
+ import withNetworkErrors from './withNetworkErrors.js';
21
+
22
+ test('a call the platform fetch rejects is marked as a network failure', async () => {
23
+ const failure = new TypeError('Network request failed');
24
+ const fetch = withNetworkErrors(jest.fn(async () => Promise.reject(failure)));
25
+ const error = await fetch('https://app.example.com/api/user', {}).catch((caught) => caught);
26
+ expect(error).toBe(failure);
27
+ expect(isNetworkError(error)).toBe(true);
28
+ });
29
+
30
+ test('expo/fetch failures, which are not TypeErrors, are marked too', async () => {
31
+ const failure = new Error('fetch failed: The Internet connection appears to be offline.');
32
+ const fetch = withNetworkErrors(jest.fn(async () => Promise.reject(failure)));
33
+ const error = await fetch('https://app.example.com/api/agent', {}).catch((caught) => caught);
34
+ expect(isNetworkError(error)).toBe(true);
35
+ });
36
+
37
+ test('a call the caller aborted is not a network failure', async () => {
38
+ const controller = new AbortController();
39
+ controller.abort();
40
+ const failure = new Error('The operation was aborted.');
41
+ const fetch = withNetworkErrors(jest.fn(async () => Promise.reject(failure)));
42
+ const error = await fetch('https://app.example.com/api/agent', {
43
+ signal: controller.signal,
44
+ }).catch((caught) => caught);
45
+ expect(isNetworkError(error)).toBe(false);
46
+ });
47
+
48
+ test('a TypeError thrown after the answer arrived is not a network failure', async () => {
49
+ const response = { ok: true, status: 200 };
50
+ const fetch = withNetworkErrors(jest.fn(async () => response));
51
+ const res = await fetch('https://app.example.com/api/user', {});
52
+ expect(res).toBe(response);
53
+ let fault;
54
+ try {
55
+ res.headers.get('etag');
56
+ } catch (error) {
57
+ fault = error;
58
+ }
59
+ expect(fault).toBeInstanceOf(TypeError);
60
+ expect(isNetworkError(fault)).toBe(false);
61
+ });
@@ -0,0 +1,25 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // Every call a shell makes carries x-lowdefy-shell, its version: the server serves mobile pages
18
+ // only to shells and web pages only to browsers.
19
+ function withShellHeader({ fetch, shellVersion }) {
20
+ return function shellHeaderFetch(url, init = {}) {
21
+ return fetch(url, { ...init, headers: { ...init.headers, 'x-lowdefy-shell': shellVersion } });
22
+ };
23
+ }
24
+
25
+ export default withShellHeader;
@@ -0,0 +1,28 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { jest } from '@jest/globals';
18
+
19
+ import withShellHeader from './withShellHeader.js';
20
+
21
+ test('every call carries the shell version as x-lowdefy-shell', async () => {
22
+ const fetch = jest.fn(async () => ({ status: 200 }));
23
+ const shellFetch = withShellHeader({ fetch, shellVersion: '7.4.0' });
24
+ await shellFetch('https://app.example.com/api/page/home', { headers: { Accept: 'x' } });
25
+ await shellFetch('https://app.example.com/api/user');
26
+ expect(fetch.mock.calls[0][1].headers).toEqual({ Accept: 'x', 'x-lowdefy-shell': '7.4.0' });
27
+ expect(fetch.mock.calls[1][1].headers).toEqual({ 'x-lowdefy-shell': '7.4.0' });
28
+ });
@@ -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 { type } from '@lowdefy/helpers';
18
+
19
+ /**
20
+ * The one place the shell turns `_js` source into functions. A shell is shared by every app, so
21
+ * it cannot carry an app's compiled clientJsMap: each mobile page, and the mobile root for the
22
+ * app events, carries its `_js` as `_jsEntries`, the module text the build generates
23
+ * (`export default { [hash]: fn }`). It is evaluated with `new Function`, as the dev server
24
+ * evaluates a page it builds on request (server-dev usePageConfig), and keeps the `volatile` flag
25
+ * the module sets for dependency tracking.
26
+ *
27
+ * Hermes must run `new Function` for this to work. Should it not, `_js` on mobile pages becomes a
28
+ * build error, and this function the one place that refuses it.
29
+ *
30
+ * @param {string|null|undefined} jsEntries The module text; none when the page has no `_js`.
31
+ * @returns {Object} `{ [hash]: fn }`, the jsMap the engine's `_js` operator reads.
32
+ */
33
+ function evaluateJsEntries(jsEntries) {
34
+ if (type.isNone(jsEntries)) {
35
+ return {};
36
+ }
37
+ const define = new Function('exports', jsEntries.replace('export default', 'exports.default ='));
38
+ const module = {};
39
+ define(module);
40
+ return module.default;
41
+ }
42
+
43
+ export default evaluateJsEntries;
@@ -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 evaluateJsEntries from './evaluateJsEntries.js';
18
+ import helloJsEntries from '../../fixtures/hello.jsEntries.json';
19
+
20
+ const accessors = {
21
+ lowdefyGlobal: (key) => ({ greeting: 'Hello from Lowdefy' })[key],
22
+ };
23
+
24
+ test('evaluateJsEntries turns the hello page module text into its _js functions', () => {
25
+ const jsMap = evaluateJsEntries(helloJsEntries.today);
26
+ expect(Object.keys(jsMap)).toEqual([
27
+ 'HorPchxU3cTNYhB6dbx9DFcepx4=',
28
+ 'b+IYcPY3kW9J7YdZ4RyICtelfz8=',
29
+ ]);
30
+ expect(jsMap['HorPchxU3cTNYhB6dbx9DFcepx4='](accessors)).toBe('Hello from Lowdefy');
31
+ expect(jsMap['b+IYcPY3kW9J7YdZ4RyICtelfz8='](accessors)).toBe(String(new Date().getFullYear()));
32
+ });
33
+
34
+ test('evaluateJsEntries keeps the volatile flag dependency tracking reads', () => {
35
+ const jsMap = evaluateJsEntries(helloJsEntries.today);
36
+ expect(jsMap['b+IYcPY3kW9J7YdZ4RyICtelfz8='].volatile).toBe(true);
37
+ expect(jsMap['HorPchxU3cTNYhB6dbx9DFcepx4='].volatile).toBeUndefined();
38
+ });
39
+
40
+ test('evaluateJsEntries evaluates the app events module text of the root', () => {
41
+ const jsMap = evaluateJsEntries(helloJsEntries.app);
42
+ expect(jsMap['yHZGzdxAuSQ8P5USi0AFFY81iNg='](accessors)).toBe('Hello from Lowdefy');
43
+ });
44
+
45
+ test('evaluateJsEntries gives an empty map for a page without _js', () => {
46
+ expect(evaluateJsEntries(undefined)).toEqual({});
47
+ expect(evaluateJsEntries(null)).toEqual({});
48
+ });
@@ -0,0 +1,112 @@
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 { appStorageKey, type } from '@lowdefy/helpers';
18
+
19
+ import loadDayjsLocale from './loadDayjsLocale.js';
20
+ import resolveActiveLocale from './resolveActiveLocale.js';
21
+
22
+ /**
23
+ * The locale of one app in the shell, kept as the web client keeps it (useLocale): the app's
24
+ * i18n config from the mobile root, the person's choice that SetLocale sets and the device store
25
+ * keeps under the app's key, and the device's locale.
26
+ *
27
+ * `apply` points the app at its active locale: `lowdefy.i18n` (read by `_t`, `_locale` and
28
+ * translate), the window members the engine trace reads, and the dayjs locale `_dayjs` formats
29
+ * with. Screens call it as they render and re-render on `subscribe`.
30
+ *
31
+ * It sets `window.__lowdefy_setLocale`, which actions-core's SetLocale calls.
32
+ *
33
+ * @param {Object} params
34
+ * @param {() => string[]} params.getDeviceLocales
35
+ * @param {Object} params.globals The app's host globals.
36
+ * @param {Object} params.session The app's session (createAppSession).
37
+ * @param {Object} params.storage The device store.
38
+ */
39
+ function createShellLocale({ getDeviceLocales, globals, session, storage }) {
40
+ const listeners = new Set();
41
+ let version = 0;
42
+
43
+ function getStorageKey() {
44
+ const { rootConfig } = session.getState();
45
+ return appStorageKey({
46
+ basePath: '',
47
+ name: 'lowdefy_locale',
48
+ slug: rootConfig.lowdefyApp.slug,
49
+ });
50
+ }
51
+
52
+ // A host store may throw when the platform blocks storage, as the core allows; the choice then
53
+ // lasts only while the app runs.
54
+ function readPreference() {
55
+ try {
56
+ return storage.get(getStorageKey()) ?? 'auto';
57
+ } catch {
58
+ return 'auto';
59
+ }
60
+ }
61
+
62
+ function writePreference(preference) {
63
+ try {
64
+ if (preference === 'auto') {
65
+ storage.remove(getStorageKey());
66
+ return;
67
+ }
68
+ storage.set(getStorageKey(), preference);
69
+ } catch {
70
+ // See readPreference.
71
+ }
72
+ }
73
+
74
+ function apply() {
75
+ const { lowdefy, rootConfig } = session.getState();
76
+ const { i18n } = rootConfig;
77
+ if (type.isNone(i18n.defaultLocale)) {
78
+ return;
79
+ }
80
+ const active = resolveActiveLocale({
81
+ deviceLocales: getDeviceLocales(),
82
+ i18n,
83
+ preference: readPreference(),
84
+ });
85
+ lowdefy.i18n = { ...i18n, active };
86
+ globals.window.__lowdefy_locale = active;
87
+ globals.window.__lowdefy_supported_locales = i18n.locales;
88
+ const dayjsId = i18n.locales.find((locale) => locale.code === active)?.dayjs;
89
+ if (type.isString(dayjsId)) {
90
+ loadDayjsLocale(dayjsId);
91
+ }
92
+ }
93
+
94
+ // Applied now, not on the next render: SetLocale updates the engine straight after.
95
+ function setPreference(code) {
96
+ writePreference(code ?? 'auto');
97
+ apply();
98
+ version += 1;
99
+ listeners.forEach((listener) => listener());
100
+ }
101
+
102
+ function subscribe(listener) {
103
+ listeners.add(listener);
104
+ return () => listeners.delete(listener);
105
+ }
106
+
107
+ globals.window.__lowdefy_setLocale = setPreference;
108
+
109
+ return { apply, getVersion: () => version, setPreference, subscribe };
110
+ }
111
+
112
+ export default createShellLocale;
@@ -0,0 +1,74 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { jest } from '@jest/globals';
18
+
19
+ import createMemoryStore from '../host/createMemoryStore.js';
20
+ import createShellLocale from './createShellLocale.js';
21
+
22
+ const i18n = {
23
+ defaultLocale: 'en-US',
24
+ locales: [
25
+ { code: 'en-US', dayjs: 'en' },
26
+ { code: 'af-ZA', dayjs: 'af' },
27
+ ],
28
+ messages: {},
29
+ };
30
+
31
+ function createLocale(rootI18n) {
32
+ const lowdefy = {};
33
+ const session = {
34
+ getState: () => ({ lowdefy, rootConfig: { i18n: rootI18n, lowdefyApp: { slug: 'field' } } }),
35
+ };
36
+ const globals = { window: {} };
37
+ const storage = createMemoryStore();
38
+ const locale = createShellLocale({
39
+ getDeviceLocales: () => ['en-ZA'],
40
+ globals,
41
+ session,
42
+ storage,
43
+ });
44
+ return { globals, locale, lowdefy, storage };
45
+ }
46
+
47
+ test('createShellLocale points lowdefy.i18n and the window at the active locale', () => {
48
+ const { globals, locale, lowdefy } = createLocale(i18n);
49
+ locale.apply();
50
+ expect(lowdefy.i18n).toEqual({ ...i18n, active: 'en-US' });
51
+ expect(globals.window.__lowdefy_locale).toBe('en-US');
52
+ expect(globals.window.__lowdefy_supported_locales).toBe(i18n.locales);
53
+ });
54
+
55
+ test('SetLocale sets the choice through the window, keeps it and tells the screens', () => {
56
+ const { globals, locale, lowdefy, storage } = createLocale(i18n);
57
+ const listener = jest.fn();
58
+ locale.subscribe(listener);
59
+ globals.window.__lowdefy_setLocale('af-ZA');
60
+ expect(lowdefy.i18n.active).toBe('af-ZA');
61
+ expect(storage.get('lowdefy:field::lowdefy_locale')).toBe('af-ZA');
62
+ expect(listener).toHaveBeenCalledTimes(1);
63
+ expect(locale.getVersion()).toBe(1);
64
+ globals.window.__lowdefy_setLocale('auto');
65
+ expect(storage.keys()).toEqual([]);
66
+ expect(lowdefy.i18n.active).toBe('en-US');
67
+ });
68
+
69
+ test('createShellLocale leaves an app without i18n alone', () => {
70
+ const { globals, locale, lowdefy } = createLocale({});
71
+ locale.apply();
72
+ expect(lowdefy.i18n).toBeUndefined();
73
+ expect(globals.window.__lowdefy_locale).toBeUndefined();
74
+ });