@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,68 @@
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 createDevShellConfig from '../connection/createDevShellConfig.js';
20
+ import createDeviceInjections from './createDeviceInjections.js';
21
+ import createFakeNetwork from '../../test/createFakeNetwork.js';
22
+ import createNetworkStatus from '../../host/createNetworkStatus.js';
23
+ import createOfflineFetch from './createOfflineFetch.js';
24
+
25
+ const shell = { environment: 'dev', version: '7.4.0' };
26
+
27
+ function setup({ pair = null } = {}) {
28
+ const injections = createDeviceInjections({
29
+ networkStatus: createNetworkStatus({ network: createFakeNetwork() }),
30
+ });
31
+ const fetch = jest.fn(async () => ({ ok: true }));
32
+ const devShell = createDevShellConfig({
33
+ fetch: createOfflineFetch({ fetch, isOffline: injections.isOffline }),
34
+ server: { url: 'http://192.168.1.20:3000', host: '192.168.1.20:3000', pair },
35
+ shell,
36
+ WebSocket: class {},
37
+ });
38
+ return { devShell, fetch, injections };
39
+ }
40
+
41
+ test("offline, the app's every fetch fails as React Native's fetch fails with no network", async () => {
42
+ const { devShell, fetch, injections } = setup({ pair: 'token' });
43
+ await devShell.transport.fetch('http://192.168.1.20:3000/api/page/home');
44
+ expect(fetch).toHaveBeenCalledTimes(1);
45
+
46
+ injections.inject({ kind: 'network', value: 'offline' });
47
+ const failed = devShell.transport.fetch('http://192.168.1.20:3000/api/request/home/load');
48
+ await expect(failed).rejects.toThrow(TypeError);
49
+ await expect(failed).rejects.toThrow('Network request failed');
50
+ expect(fetch).toHaveBeenCalledTimes(1);
51
+
52
+ injections.inject({ kind: 'network', value: 'online' });
53
+ await devShell.transport.fetch('http://192.168.1.20:3000/api/page/home');
54
+ expect(fetch).toHaveBeenCalledTimes(2);
55
+ });
56
+
57
+ test('offline, the dev-tab client still reaches the dev server, so an agent can go back online', async () => {
58
+ const { devShell, injections } = setup({ pair: 'token' });
59
+ injections.inject({ kind: 'network', value: 'offline' });
60
+ const tabFetch = jest.fn(async () => ({ ok: true }));
61
+ await devShell.transport.wrapFetch(tabFetch)('http://192.168.1.20:3000/api/dev-inspect', {
62
+ method: 'POST',
63
+ });
64
+ expect(tabFetch).toHaveBeenCalledWith('http://192.168.1.20:3000/api/dev-inspect', {
65
+ method: 'POST',
66
+ headers: { 'x-lowdefy-pair': 'token' },
67
+ });
68
+ });
@@ -0,0 +1,60 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import * as Notifications from 'expo-notifications';
18
+ import { type } from '@lowdefy/helpers';
19
+
20
+ import { getCurrentApp } from '../../native/currentApp.js';
21
+ import handleNotificationResponse from '../../notifications/handleNotificationResponse.js';
22
+
23
+ /**
24
+ * Lowdefy Dev's push injection: shows `{ title, body, data, categoryId }` as a notification now,
25
+ * as a push arriving with the app open would (the app's onNotification fires). With `open: true`
26
+ * it is then tapped (data.path opens, onNotificationAction fires with actionId default); with
27
+ * `actionId` (and `text` for a reply) that button is pressed instead, as the person would.
28
+ *
29
+ * @param {Object} value
30
+ */
31
+ async function injectPush(value) {
32
+ if (!type.isObject(value) || !(type.isString(value.title) || type.isString(value.body))) {
33
+ throw new Error(
34
+ `A push injection needs { title, body, data?, categoryId?, open?, actionId?, text? }. Received ${JSON.stringify(
35
+ value
36
+ )}.`
37
+ );
38
+ }
39
+ const content = {
40
+ title: value.title,
41
+ body: value.body,
42
+ data: value.data ?? {},
43
+ categoryIdentifier: value.categoryId,
44
+ };
45
+ const id = await Notifications.scheduleNotificationAsync({ content, trigger: null });
46
+ if (value.open !== true && !type.isString(value.actionId)) {
47
+ return { ok: true, kind: 'push', id };
48
+ }
49
+ await handleNotificationResponse({
50
+ app: await getCurrentApp(),
51
+ response: {
52
+ actionIdentifier: value.actionId ?? Notifications.DEFAULT_ACTION_IDENTIFIER,
53
+ notification: { request: { identifier: id, content } },
54
+ userText: value.text,
55
+ },
56
+ });
57
+ return { ok: true, kind: 'push', id, responded: value.actionId ?? 'default' };
58
+ }
59
+
60
+ export default injectPush;
@@ -0,0 +1,55 @@
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
+ import { getCurrentApp } from '../../native/currentApp.js';
20
+
21
+ /**
22
+ * Lowdefy Dev's share injection: as if `{ text?, url?, files? }` were shared into the app from
23
+ * another app, so mobile.share.pageId opens with it and onShare fires. Files are
24
+ * [{ uri, name, mimeType, size? }].
25
+ *
26
+ * @param {Object} value
27
+ */
28
+ async function injectShare(value) {
29
+ if (
30
+ !type.isObject(value) ||
31
+ ![value.text, value.url, value.files].some((item) => !type.isNone(item))
32
+ ) {
33
+ throw new Error(
34
+ `A share injection needs { text?, url?, files? }. Received ${JSON.stringify(value)}.`
35
+ );
36
+ }
37
+ const payloads = [
38
+ ...(type.isString(value.text) ? [{ shareType: 'text', value: value.text }] : []),
39
+ ...(type.isString(value.url) ? [{ shareType: 'url', value: value.url }] : []),
40
+ ...(value.files ?? []).map((file) => ({
41
+ shareType: 'file',
42
+ value: file.uri,
43
+ contentUri: file.uri,
44
+ contentType: 'file',
45
+ contentMimeType: file.mimeType ?? null,
46
+ originalName: file.name ?? null,
47
+ contentSize: file.size ?? null,
48
+ })),
49
+ ];
50
+ const app = await getCurrentApp();
51
+ await app.receiveShare({ payloads });
52
+ return { ok: true, kind: 'share' };
53
+ }
54
+
55
+ export default injectShare;
@@ -0,0 +1,98 @@
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 { FEATURE_SHELLS } from '@lowdefy/blocks-native/device';
18
+
19
+ // An injected photo as a picked file: the PickImage shape, with its content inline.
20
+ function toPhotoFile(photo) {
21
+ const mimeType = photo.mimeType ?? 'image/jpeg';
22
+ return {
23
+ name: photo.name ?? 'injected-photo.jpg',
24
+ mimeType,
25
+ size: Math.round((photo.base64.length * 3) / 4),
26
+ uri: `data:${mimeType};base64,${photo.base64}`,
27
+ width: photo.width ?? null,
28
+ height: photo.height ?? null,
29
+ base64: photo.base64,
30
+ };
31
+ }
32
+
33
+ // A feature method that answers an injected result first, and asks the shell's own feature
34
+ // otherwise. Lowdefy Dev carries the field profile, so NFC has no feature of its own there: an
35
+ // injection is then the only answer, and a read with none throws as a shell without NFC does.
36
+ function answerFirst({ action, feature, method, read, real, toResult }) {
37
+ return async (params) => {
38
+ const value = read();
39
+ if (value !== undefined) {
40
+ return toResult(value);
41
+ }
42
+ if (real === undefined) {
43
+ throw new Error(
44
+ `${action} needs a shell that carries ${feature} (${FEATURE_SHELLS[feature]}). In Lowdefy Dev, inject one.`
45
+ );
46
+ }
47
+ return real[method](params);
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Lowdefy Dev's device: the shell's features, with the next scan, photo and NFC read answered by
53
+ * what an agent or journey injected (src/device/injected.js) before the camera or the tag reader.
54
+ *
55
+ * @param {Object} params
56
+ * @param {Object} params.features The shell's device features (src/device/device.js).
57
+ * @param {(name: string) => any} params.read injected().
58
+ */
59
+ function withInjectedDevice({ features, read }) {
60
+ const photo = (method, feature, action) =>
61
+ answerFirst({
62
+ action,
63
+ feature,
64
+ method,
65
+ read: () => read('photo'),
66
+ real: features[feature],
67
+ toResult: (value) => [toPhotoFile(value)],
68
+ });
69
+ return {
70
+ ...features,
71
+ camera: {
72
+ ...features.camera,
73
+ scanCode: answerFirst({
74
+ action: 'ScanCode',
75
+ feature: 'camera',
76
+ method: 'scanCode',
77
+ read: () => read('scan'),
78
+ real: features.camera,
79
+ toResult: (value) => ({ data: value.data, type: value.type ?? 'qr' }),
80
+ }),
81
+ takePhoto: photo('takePhoto', 'camera', 'TakePhoto'),
82
+ },
83
+ photos: { ...features.photos, pick: photo('pick', 'photos', 'PickImage') },
84
+ nfc: {
85
+ ...features.nfc,
86
+ read: answerFirst({
87
+ action: 'ReadNfc',
88
+ feature: 'nfc',
89
+ method: 'read',
90
+ read: () => read('nfc'),
91
+ real: features.nfc,
92
+ toResult: (value) => value,
93
+ }),
94
+ },
95
+ };
96
+ }
97
+
98
+ export default withInjectedDevice;
@@ -0,0 +1,33 @@
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 { Text, View } from 'react-native';
19
+
20
+ import devStyles from '../devStyles.js';
21
+
22
+ // The shake menu's block gallery: every native block the shell carries, shown with its examples.
23
+ // The native block set fills it in; until then it says so.
24
+ function BlockGallery() {
25
+ return (
26
+ <View style={devStyles.screen} testID="dev-block-gallery">
27
+ <Text style={devStyles.heading}>Block gallery</Text>
28
+ <Text style={devStyles.hint}>The gallery of native blocks is not in this build yet.</Text>
29
+ </View>
30
+ );
31
+ }
32
+
33
+ export default BlockGallery;
@@ -0,0 +1,121 @@
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, { useState, useSyncExternalStore } from 'react';
18
+ import { Modal, Pressable, ScrollView, Text, View } from 'react-native';
19
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
20
+
21
+ import BlockGallery from './BlockGallery.js';
22
+ import devStyles from '../devStyles.js';
23
+
24
+ function MenuItem({ hint, label, onPress, testID }) {
25
+ return (
26
+ <Pressable accessibilityRole="button" onPress={onPress} style={devStyles.row} testID={testID}>
27
+ <Text style={devStyles.rowText}>{label}</Text>
28
+ {hint ? <Text style={devStyles.hint}>{hint}</Text> : null}
29
+ </Pressable>
30
+ );
31
+ }
32
+
33
+ function LogList({ logs }) {
34
+ const entries = useSyncExternalStore(logs.subscribe, logs.entries);
35
+ if (entries.length === 0) {
36
+ return <Text style={devStyles.hint}>Nothing logged yet.</Text>;
37
+ }
38
+ return entries.map((entry, index) => (
39
+ <Text key={index} style={devStyles.mono}>
40
+ {`${entry.at.toISOString().slice(11, 19)} ${entry.level} ${entry.message}`}
41
+ </Text>
42
+ ));
43
+ }
44
+
45
+ /**
46
+ * The shake menu: reload, switch server, clear data, the shell's logs, the block gallery and the
47
+ * shell's version.
48
+ *
49
+ * @param {Object} props
50
+ * @param {boolean} props.visible
51
+ * @param {() => void} props.onClose
52
+ * @param {() => void} props.onReload
53
+ * @param {() => void} props.onSwitchServer
54
+ * @param {() => void} props.onClearData
55
+ * @param {Object} props.logs The log buffer (createLogBuffer).
56
+ * @param {string|null} props.server The host of the server Lowdefy Dev is connected to.
57
+ * @param {Object} props.shell The shell config: name and version.
58
+ */
59
+ function DevMenu({ logs, onClearData, onClose, onReload, onSwitchServer, server, shell, visible }) {
60
+ const insets = useSafeAreaInsets();
61
+ const [panel, setPanel] = useState('menu');
62
+ function close() {
63
+ setPanel('menu');
64
+ onClose();
65
+ }
66
+ function act(action) {
67
+ close();
68
+ action();
69
+ }
70
+ return (
71
+ <Modal animationType="slide" onRequestClose={close} transparent visible={visible}>
72
+ <View style={devStyles.backdrop}>
73
+ <ScrollView
74
+ contentContainerStyle={[devStyles.sheet, { paddingBottom: insets.bottom + 24 }]}
75
+ style={{ maxHeight: '85%', flexGrow: 0 }}
76
+ testID="dev-menu"
77
+ >
78
+ {panel === 'menu' ? (
79
+ <>
80
+ <Text style={devStyles.sheetTitle}>{shell.name}</Text>
81
+ <Text style={devStyles.hint} testID="dev-menu-version">
82
+ {`Shell ${shell.version}${server ? ` · ${server}` : ''}`}
83
+ </Text>
84
+ <MenuItem label="Reload" onPress={() => act(onReload)} testID="dev-menu-reload" />
85
+ <MenuItem
86
+ hint={server ?? undefined}
87
+ label="Switch server"
88
+ onPress={() => act(onSwitchServer)}
89
+ testID="dev-menu-switch"
90
+ />
91
+ <MenuItem
92
+ hint="Signs out and removes this server's cached pages and storage"
93
+ label="Clear data"
94
+ onPress={() => act(onClearData)}
95
+ testID="dev-menu-clear"
96
+ />
97
+ <MenuItem label="Show logs" onPress={() => setPanel('logs')} testID="dev-menu-logs" />
98
+ <MenuItem
99
+ label="Block gallery"
100
+ onPress={() => setPanel('gallery')}
101
+ testID="dev-menu-gallery"
102
+ />
103
+ </>
104
+ ) : null}
105
+ {panel === 'logs' ? <LogList logs={logs} /> : null}
106
+ {panel === 'gallery' ? <BlockGallery /> : null}
107
+ <Pressable
108
+ accessibilityRole="button"
109
+ onPress={panel === 'menu' ? close : () => setPanel('menu')}
110
+ style={devStyles.secondaryButton}
111
+ testID="dev-menu-close"
112
+ >
113
+ <Text style={devStyles.secondaryButtonText}>{panel === 'menu' ? 'Close' : 'Back'}</Text>
114
+ </Pressable>
115
+ </ScrollView>
116
+ </View>
117
+ </Modal>
118
+ );
119
+ }
120
+
121
+ export default DevMenu;
@@ -0,0 +1,83 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ const LEVELS = ['log', 'info', 'warn', 'error'];
18
+
19
+ function formatPart(part) {
20
+ if (part instanceof Error) {
21
+ return part.message;
22
+ }
23
+ if (typeof part === 'string') {
24
+ return part;
25
+ }
26
+ try {
27
+ return JSON.stringify(part);
28
+ } catch (error) {
29
+ return String(part);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * The shell's recent console output, for the shake menu's "Show logs": the last `limit` lines,
35
+ * `{ at, level, message }`, oldest first. install() copies every console call into it (the console
36
+ * still prints); it returns the uninstall function.
37
+ *
38
+ * @param {Object} params
39
+ * @param {Object} params.console The console to follow.
40
+ * @param {number} [params.limit]
41
+ */
42
+ function createLogBuffer({ console, limit = 200 }) {
43
+ // Replaced, never changed, on each line: a snapshot for useSyncExternalStore.
44
+ let entries = [];
45
+ const listeners = new Set();
46
+
47
+ function add({ level, parts }) {
48
+ const entry = { at: new Date(), level, message: parts.map(formatPart).join(' ') };
49
+ entries = [...entries, entry].slice(-limit);
50
+ // A component may log while React renders another: the log view hears of it afterwards.
51
+ setTimeout(() => listeners.forEach((listener) => listener()), 0);
52
+ }
53
+
54
+ function install() {
55
+ const originals = {};
56
+ LEVELS.forEach((level) => {
57
+ originals[level] = console[level];
58
+ console[level] = (...parts) => {
59
+ add({ level, parts });
60
+ originals[level].apply(console, parts);
61
+ };
62
+ });
63
+ return () => {
64
+ LEVELS.forEach((level) => {
65
+ console[level] = originals[level];
66
+ });
67
+ };
68
+ }
69
+
70
+ function subscribe(listener) {
71
+ listeners.add(listener);
72
+ return () => listeners.delete(listener);
73
+ }
74
+
75
+ return {
76
+ add: (level, ...parts) => add({ level, parts }),
77
+ entries: () => entries,
78
+ install,
79
+ subscribe,
80
+ };
81
+ }
82
+
83
+ export default createLogBuffer;
@@ -0,0 +1,52 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ const UPDATE_MS = 100;
18
+ // In g: a phone at rest reads about 1.
19
+ const SHAKE_FORCE = 1.8;
20
+ const JOLTS = 3;
21
+ const WINDOW_MS = 800;
22
+ const QUIET_MS = 1500;
23
+
24
+ /**
25
+ * Calls onShake when the phone is shaken: 3 jolts over 1.8 g within 0.8 s, then not again for 1.5 s.
26
+ * Returns the function that stops listening.
27
+ *
28
+ * @param {Object} params
29
+ * @param {Object} params.accelerometer expo-sensors' Accelerometer.
30
+ * @param {() => void} params.onShake
31
+ * @param {() => number} [params.now]
32
+ */
33
+ function watchShake({ accelerometer, now = Date.now, onShake }) {
34
+ let jolts = [];
35
+ let quietUntil = 0;
36
+ accelerometer.setUpdateInterval(UPDATE_MS);
37
+ const subscription = accelerometer.addListener(({ x, y, z }) => {
38
+ const time = now();
39
+ if (time < quietUntil || Math.sqrt(x * x + y * y + z * z) < SHAKE_FORCE) {
40
+ return;
41
+ }
42
+ jolts = [...jolts.filter((at) => time - at <= WINDOW_MS), time];
43
+ if (jolts.length >= JOLTS) {
44
+ jolts = [];
45
+ quietUntil = time + QUIET_MS;
46
+ onShake();
47
+ }
48
+ });
49
+ return () => subscription.remove();
50
+ }
51
+
52
+ export default watchShake;
@@ -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 watchShake from './watchShake.js';
20
+
21
+ function setup() {
22
+ let listener = null;
23
+ let time = 0;
24
+ const onShake = jest.fn();
25
+ const stop = watchShake({
26
+ accelerometer: {
27
+ setUpdateInterval: jest.fn(),
28
+ addListener: (fn) => {
29
+ listener = fn;
30
+ return { remove: jest.fn() };
31
+ },
32
+ },
33
+ now: () => time,
34
+ onShake,
35
+ });
36
+ function read(g, at) {
37
+ time = at;
38
+ listener({ x: g, y: 0, z: 0 });
39
+ }
40
+ return { onShake, read, stop };
41
+ }
42
+
43
+ test('three jolts within 0.8 s open the menu once, then it rests for 1.5 s', () => {
44
+ const { onShake, read } = setup();
45
+ read(2.5, 0);
46
+ read(2.5, 200);
47
+ expect(onShake).not.toHaveBeenCalled();
48
+ read(2.5, 400);
49
+ expect(onShake).toHaveBeenCalledTimes(1);
50
+ [500, 600, 700].forEach((at) => read(2.5, at));
51
+ expect(onShake).toHaveBeenCalledTimes(1);
52
+ [2000, 2100, 2200].forEach((at) => read(2.5, at));
53
+ expect(onShake).toHaveBeenCalledTimes(2);
54
+ });
55
+
56
+ test('a phone at rest, or jolts spread out, do not open the menu', () => {
57
+ const { onShake, read } = setup();
58
+ [0, 100, 200, 300].forEach((at) => read(1, at));
59
+ [1000, 2000, 3000].forEach((at) => read(2.5, at));
60
+ expect(onShake).not.toHaveBeenCalled();
61
+ });