@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,89 @@
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, { useRef } from 'react';
18
+ import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
19
+ import { CameraView, useCameraPermissions } from 'expo-camera';
20
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
21
+
22
+ import devStyles from '../devStyles.js';
23
+
24
+ const styles = StyleSheet.create({
25
+ camera: { flex: 1 },
26
+ footer: { padding: 24, gap: 12, backgroundColor: '#ffffff' },
27
+ });
28
+
29
+ /**
30
+ * Scans the QR code `lowdefy dev --lan` prints, with expo-camera. onScanned gets the code's text
31
+ * once; the caller reads it as a link.
32
+ *
33
+ * @param {Object} props
34
+ * @param {boolean} props.visible
35
+ * @param {(text: string) => void} props.onScanned
36
+ * @param {() => void} props.onClose
37
+ */
38
+ function QrScanner({ onClose, onScanned, visible }) {
39
+ const [permission, requestPermission] = useCameraPermissions();
40
+ const scanned = useRef(false);
41
+ const insets = useSafeAreaInsets();
42
+ if (!visible) {
43
+ scanned.current = false;
44
+ }
45
+ const granted = permission?.granted === true;
46
+ return (
47
+ <Modal animationType="slide" onRequestClose={onClose} visible={visible}>
48
+ <View style={{ flex: 1, paddingTop: insets.top }} testID="dev-qr-scanner">
49
+ {granted ? (
50
+ <CameraView
51
+ barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
52
+ onBarcodeScanned={({ data }) => {
53
+ if (scanned.current) return;
54
+ scanned.current = true;
55
+ onScanned(data);
56
+ }}
57
+ style={styles.camera}
58
+ />
59
+ ) : (
60
+ <View style={devStyles.screen}>
61
+ <Text style={devStyles.hint}>
62
+ Lowdefy Dev uses the camera only to read the code lowdefy dev prints.
63
+ </Text>
64
+ <Pressable
65
+ accessibilityRole="button"
66
+ onPress={requestPermission}
67
+ style={devStyles.button}
68
+ testID="dev-qr-allow"
69
+ >
70
+ <Text style={devStyles.buttonText}>Allow the camera</Text>
71
+ </Pressable>
72
+ </View>
73
+ )}
74
+ <View style={[styles.footer, { paddingBottom: insets.bottom + 24 }]}>
75
+ <Pressable
76
+ accessibilityRole="button"
77
+ onPress={onClose}
78
+ style={devStyles.secondaryButton}
79
+ testID="dev-qr-close"
80
+ >
81
+ <Text style={devStyles.secondaryButtonText}>Close</Text>
82
+ </Pressable>
83
+ </View>
84
+ </View>
85
+ </Modal>
86
+ );
87
+ }
88
+
89
+ export default QrScanner;
@@ -0,0 +1,47 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import createDevTransport from './createDevTransport.js';
18
+
19
+ /**
20
+ * Lowdefy Dev's shell config for one dev server: its one app is that server, named by its host,
21
+ * and its calls carry the server's pairing token (shell.transport, read by createShellApp). Each
22
+ * server is its own app, so its cache, session token and storage are kept apart from another's.
23
+ *
24
+ * @param {Object} params
25
+ * @param {Object} params.server `{ url, host, pair }`.
26
+ * @param {Object} params.shell The shell config (getShellConfig).
27
+ * @param {Function} [params.fetch] The Fetch API.
28
+ * @param {Function} [params.WebSocket] The WebSocket class.
29
+ */
30
+ function createDevShellConfig({
31
+ fetch = globalThis.fetch,
32
+ server,
33
+ shell,
34
+ WebSocket = globalThis.WebSocket,
35
+ }) {
36
+ return {
37
+ ...shell,
38
+ app: {
39
+ id: `dev-${server.host.replace(/[^A-Za-z0-9._-]/g, '_')}`,
40
+ name: server.host,
41
+ urls: { [shell.environment]: server.url },
42
+ },
43
+ transport: createDevTransport({ fetch, pair: server.pair, WebSocket }),
44
+ };
45
+ }
46
+
47
+ export default createDevShellConfig;
@@ -0,0 +1,51 @@
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 PAIR_HEADER = 'x-lowdefy-pair';
18
+
19
+ /**
20
+ * How calls reach a dev server: `{ fetch, WebSocket, wrapFetch }`, sending the pairing token of
21
+ * `lowdefy dev --lan` as x-lowdefy-pair on every request and websocket upgrade. wrapFetch adds it
22
+ * to another Fetch API (the dev-tab client's streaming one). Without a token (a simulator, a USB
23
+ * phone) they are the platform's own.
24
+ *
25
+ * @param {Object} params
26
+ * @param {Function} params.fetch The Fetch API.
27
+ * @param {Function} params.WebSocket The WebSocket class (React Native's takes headers).
28
+ * @param {string|null} params.pair The pairing token.
29
+ */
30
+ function createDevTransport({ fetch, pair, WebSocket }) {
31
+ if (pair === null) {
32
+ return { fetch, WebSocket, wrapFetch: (baseFetch) => baseFetch };
33
+ }
34
+ function wrapFetch(baseFetch) {
35
+ return function pairedFetch(url, init = {}) {
36
+ return baseFetch(url, { ...init, headers: { ...init.headers, [PAIR_HEADER]: pair } });
37
+ };
38
+ }
39
+ class PairedWebSocket extends WebSocket {
40
+ constructor(url, protocols, options = {}) {
41
+ super(url, protocols, {
42
+ ...options,
43
+ headers: { ...options.headers, [PAIR_HEADER]: pair },
44
+ });
45
+ }
46
+ }
47
+ return { fetch: wrapFetch(fetch), WebSocket: PairedWebSocket, wrapFetch };
48
+ }
49
+
50
+ export { PAIR_HEADER };
51
+ export default createDevTransport;
@@ -0,0 +1,87 @@
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 './createDevShellConfig.js';
20
+ import createDevTransport from './createDevTransport.js';
21
+ import createMemoryStore from '../../host/createMemoryStore.js';
22
+ import createServerList from './createServerList.js';
23
+
24
+ const shell = { id: 'lowdefy-dev', environment: 'dev', scheme: 'lowdefy-dev', version: '7.0.0' };
25
+
26
+ class FakeWebSocket {
27
+ constructor(url, protocols, options) {
28
+ this.args = { url, protocols, options };
29
+ }
30
+ }
31
+
32
+ test('a paired server gets the pairing token on every call and websocket upgrade', async () => {
33
+ const fetch = jest.fn(async () => ({ status: 200 }));
34
+ const transport = createDevTransport({ fetch, pair: 'k3x9q', WebSocket: FakeWebSocket });
35
+ await transport.fetch('http://192.168.1.20:3000/api/mobile/root', { headers: { a: '1' } });
36
+ expect(fetch.mock.calls[0][1].headers).toEqual({ a: '1', 'x-lowdefy-pair': 'k3x9q' });
37
+ const socket = new transport.WebSocket('ws://192.168.1.20:3000/api/websocket', undefined, {
38
+ headers: { Authorization: 'Bearer t' },
39
+ });
40
+ expect(socket).toBeInstanceOf(FakeWebSocket);
41
+ expect(socket.args.options.headers).toEqual({
42
+ Authorization: 'Bearer t',
43
+ 'x-lowdefy-pair': 'k3x9q',
44
+ });
45
+ const streamFetch = jest.fn(async () => ({ status: 200 }));
46
+ await transport.wrapFetch(streamFetch)('http://192.168.1.20:3000/api/reload');
47
+ expect(streamFetch.mock.calls[0][1].headers).toEqual({ 'x-lowdefy-pair': 'k3x9q' });
48
+ });
49
+
50
+ test('a server on this device gets the platform fetch and WebSocket, with no token', () => {
51
+ const fetch = jest.fn();
52
+ const transport = createDevTransport({ fetch, pair: null, WebSocket: FakeWebSocket });
53
+ expect(transport.fetch).toBe(fetch);
54
+ expect(transport.WebSocket).toBe(FakeWebSocket);
55
+ });
56
+
57
+ test('each dev server is its own app, at its url', () => {
58
+ const config = createDevShellConfig({
59
+ fetch: jest.fn(),
60
+ server: { url: 'http://192.168.1.20:3000', host: '192.168.1.20:3000', pair: 'k3x9q' },
61
+ shell,
62
+ WebSocket: FakeWebSocket,
63
+ });
64
+ expect(config.app).toEqual({
65
+ id: 'dev-192.168.1.20_3000',
66
+ name: '192.168.1.20:3000',
67
+ urls: { dev: 'http://192.168.1.20:3000' },
68
+ });
69
+ expect(config).not.toHaveProperty('apps');
70
+ expect(config.version).toBe('7.0.0');
71
+ });
72
+
73
+ test('the last 5 servers are remembered, the last one first', () => {
74
+ const servers = createServerList({ kv: createMemoryStore() });
75
+ [1, 2, 3, 4, 5, 6].forEach((n) => {
76
+ servers.remember({ url: `http://10.0.0.${n}:3000`, host: `10.0.0.${n}:3000`, pair: null });
77
+ });
78
+ servers.remember({ url: 'http://10.0.0.4:3000', host: '10.0.0.4:3000', pair: 'p' });
79
+ expect(servers.list().map((server) => server.host)).toEqual([
80
+ '10.0.0.4:3000',
81
+ '10.0.0.6:3000',
82
+ '10.0.0.5:3000',
83
+ '10.0.0.3:3000',
84
+ '10.0.0.2:3000',
85
+ ]);
86
+ expect(servers.last()).toEqual({ url: 'http://10.0.0.4:3000', host: '10.0.0.4:3000', pair: 'p' });
87
+ });
@@ -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 { type } from '@lowdefy/helpers';
18
+
19
+ const SERVERS_KEY = 'lowdefy-dev:servers';
20
+ const MAX_SERVERS = 5;
21
+
22
+ /**
23
+ * The dev servers Lowdefy Dev connected to, the last one first: `[{ url, host, pair }]`, at most 5.
24
+ * The pairing token is kept with its server; it lasts as long as that `lowdefy dev` session.
25
+ *
26
+ * @param {Object} params
27
+ * @param {{ get, set }} params.kv The device store.
28
+ */
29
+ function createServerList({ kv }) {
30
+ function list() {
31
+ const stored = JSON.parse(kv.get(SERVERS_KEY) ?? '[]');
32
+ return type.isArray(stored) ? stored : [];
33
+ }
34
+
35
+ function remember({ url, host, pair }) {
36
+ const servers = [
37
+ { url, host, pair: pair ?? null },
38
+ ...list().filter((server) => server.url !== url),
39
+ ].slice(0, MAX_SERVERS);
40
+ kv.set(SERVERS_KEY, JSON.stringify(servers));
41
+ return servers;
42
+ }
43
+
44
+ return { last: () => list()[0] ?? null, list, remember };
45
+ }
46
+
47
+ export { SERVERS_KEY };
48
+ export default createServerList;
@@ -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
+ // A host on this device: a simulator's localhost is the Mac's, and a USB Android phone's is the
18
+ // computer's through adb reverse. Neither reaches a server on a network.
19
+ function isLoopbackHost(host) {
20
+ const hostname = host.startsWith('[') ? host.slice(0, host.indexOf(']') + 1) : host.split(':')[0];
21
+ const name = hostname.toLowerCase();
22
+ return name === 'localhost' || name === '[::1]' || /^127\.\d+\.\d+\.\d+$/.test(name);
23
+ }
24
+
25
+ export default isLoopbackHost;
@@ -0,0 +1,104 @@
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 './createDevShellConfig.js';
20
+ import createFakeServer, { header } from '../../test/createFakeServer.js';
21
+ import createShellApp from '../../createShellApp.js';
22
+ import { createPages, createRoot } from '../../test/fixtures.js';
23
+
24
+ // createShellApp as the app runs it, over in-memory stores.
25
+ jest.mock('expo-sqlite', () => ({ openDatabaseSync: () => ({}) }));
26
+ jest.mock('../../cache/createSqliteCacheStore.js', () =>
27
+ jest.requireActual('../../test/createMemoryCacheStore.js')
28
+ );
29
+ jest.mock('../../host/createDeviceStore.js', () =>
30
+ jest.requireActual('../../host/createMemoryStore.js')
31
+ );
32
+ jest.mock('expo-secure-store', () =>
33
+ jest.requireActual('../../test/createFakeSecureStore.js').default()
34
+ );
35
+ jest.mock('expo-crypto', () => ({}));
36
+ jest.mock('expo-web-browser', () => ({}));
37
+ jest.mock('expo-local-authentication', () => ({}));
38
+ jest.mock('expo-network', () => ({ addNetworkStateListener: () => ({ remove: () => undefined }) }));
39
+ jest.mock('expo-router', () => ({ router: {} }));
40
+ jest.mock('expo-clipboard', () => ({}));
41
+ jest.mock('expo/fetch', () => ({
42
+ fetch: jest.fn(async () => ({ status: 200, headers: { get: () => null } })),
43
+ }));
44
+
45
+ class FakeWebSocket {
46
+ constructor(url, protocols, options) {
47
+ this.options = options;
48
+ }
49
+ }
50
+
51
+ const shell = {
52
+ id: 'lowdefy-dev',
53
+ environment: 'dev',
54
+ profile: 'dev',
55
+ scheme: 'lowdefy-dev',
56
+ version: '7.0.0',
57
+ };
58
+
59
+ test('every call of an app on a paired dev server carries the pairing token', async () => {
60
+ const server = createFakeServer({
61
+ root: createRoot(),
62
+ rootEtag: '"root-1"',
63
+ pages: createPages([['today', 'today', '"p-today-1"']]),
64
+ });
65
+ const devShell = createDevShellConfig({
66
+ fetch: server.fetch,
67
+ server: { url: 'http://192.168.1.20:3000', host: '192.168.1.20:3000', pair: 'k3x9q' },
68
+ shell,
69
+ WebSocket: FakeWebSocket,
70
+ });
71
+ const { auth, host, session } = await createShellApp({
72
+ onSignedOut: jest.fn(),
73
+ shell: devShell,
74
+ });
75
+ await session.sync();
76
+ await host.fetch('http://192.168.1.20:3000/api/endpoints/save', { method: 'POST' });
77
+
78
+ const calls = server.fetch.mock.calls;
79
+ expect(calls.map(([url]) => new URL(url).pathname)).toEqual([
80
+ '/api/mobile/root',
81
+ '/api/user',
82
+ '/api/page/today',
83
+ '/api/page/jobs',
84
+ '/api/page/jobs/%7Bjob_id%7D',
85
+ '/api/endpoints/save',
86
+ ]);
87
+ calls.forEach(([url, init]) => {
88
+ expect(url.startsWith('http://192.168.1.20:3000/')).toBe(true);
89
+ expect(header(init, 'x-lowdefy-pair')).toBe('k3x9q');
90
+ expect(header(init, 'x-lowdefy-shell')).toBe('7.0.0');
91
+ });
92
+ // A block's streamed call (AgentChat's chat stream) goes through expo/fetch, sent the same way.
93
+ const { fetch: expoFetch } = jest.requireMock('expo/fetch');
94
+ await host.streamFetch('http://192.168.1.20:3000/api/agent/chat/assistant', { method: 'POST' });
95
+ expect(server.fetch).toHaveBeenCalledTimes(calls.length);
96
+ const [[streamUrl, streamInit]] = expoFetch.mock.calls;
97
+ expect(streamUrl).toBe('http://192.168.1.20:3000/api/agent/chat/assistant');
98
+ expect(streamInit.credentials).toBe('omit');
99
+ expect(header(streamInit, 'x-lowdefy-pair')).toBe('k3x9q');
100
+ expect(header(streamInit, 'x-lowdefy-shell')).toBe('7.0.0');
101
+ const socket = new host.WebSocket('ws://192.168.1.20:3000/api/websocket');
102
+ expect(socket.options.headers).toEqual({ 'x-lowdefy-pair': 'k3x9q' });
103
+ auth.dispose();
104
+ });
@@ -0,0 +1,66 @@
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 parseQuery from './parseQuery.js';
20
+ import parseServerUrl from './parseServerUrl.js';
21
+
22
+ /**
23
+ * What a Lowdefy Dev link asks for, or null for any other URL (the app's own deep links):
24
+ * - `lowdefy-dev://connect?url=<server>&pair=<token>`: `{ kind: 'connect', url, host, pair }`,
25
+ * as `lowdefy dev --lan` prints it in its QR code, and `--ios`/`--android` open it (no pair).
26
+ * - `lowdefy-dev://menu`: `{ kind: 'menu' }`, the shake menu, for a simulator that cannot shake.
27
+ * A connect link without a usable url is `{ kind: 'invalid', reason }`.
28
+ *
29
+ * @param {Object} params
30
+ * @param {string} params.link The URL the app was opened with.
31
+ * @param {string} params.scheme The shell's URL scheme (lowdefy-dev).
32
+ */
33
+ function parseDevLink({ link, scheme }) {
34
+ if (!type.isString(link) || !type.isString(scheme)) {
35
+ return null;
36
+ }
37
+ const prefix = `${scheme.toLowerCase()}://`;
38
+ if (!link.toLowerCase().startsWith(prefix)) {
39
+ return null;
40
+ }
41
+ const rest = link.slice(prefix.length);
42
+ const queryIndex = rest.indexOf('?');
43
+ const target = (queryIndex === -1 ? rest : rest.slice(0, queryIndex)).replace(/\/+$/, '');
44
+ const query = parseQuery(queryIndex === -1 ? '' : rest.slice(queryIndex + 1).split('#')[0]);
45
+ if (target === 'menu') {
46
+ return { kind: 'menu' };
47
+ }
48
+ if (target !== 'connect') {
49
+ return null;
50
+ }
51
+ const server = parseServerUrl(query.url);
52
+ if (server === null) {
53
+ return {
54
+ kind: 'invalid',
55
+ reason: `The link has no dev server url. Received ${JSON.stringify(query.url ?? null)}.`,
56
+ };
57
+ }
58
+ return {
59
+ kind: 'connect',
60
+ url: server.url,
61
+ host: server.host,
62
+ pair: type.isString(query.pair) && query.pair.length > 0 ? query.pair : null,
63
+ };
64
+ }
65
+
66
+ export default parseDevLink;
@@ -0,0 +1,97 @@
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 isLoopbackHost from './isLoopbackHost.js';
18
+ import parseDevLink from './parseDevLink.js';
19
+ import parseServerInput from './parseServerInput.js';
20
+
21
+ const scheme = 'lowdefy-dev';
22
+
23
+ test('a connect link from lowdefy dev --lan gives the server and its pairing token', () => {
24
+ expect(
25
+ parseDevLink({
26
+ link: 'lowdefy-dev://connect?url=http%3A%2F%2F192.168.1.20%3A3000%2F&pair=k3x9q',
27
+ scheme,
28
+ })
29
+ ).toEqual({
30
+ kind: 'connect',
31
+ url: 'http://192.168.1.20:3000',
32
+ host: '192.168.1.20:3000',
33
+ pair: 'k3x9q',
34
+ });
35
+ });
36
+
37
+ test('a connect link from lowdefy dev --ios has no pairing token', () => {
38
+ expect(
39
+ parseDevLink({ link: 'lowdefy-dev://connect?url=http%3A%2F%2Flocalhost%3A3000', scheme })
40
+ ).toEqual({ kind: 'connect', url: 'http://localhost:3000', host: 'localhost:3000', pair: null });
41
+ });
42
+
43
+ test('a connect link whose url carries userinfo is invalid, so it never passes for loopback', () => {
44
+ [
45
+ 'http://localhost:3000@evil.example',
46
+ 'http://[::1]:1@evil.example',
47
+ 'http://localhost\\@evil.example',
48
+ ].forEach((url) => {
49
+ const result = parseDevLink({
50
+ link: `lowdefy-dev://connect?url=${encodeURIComponent(url)}`,
51
+ scheme,
52
+ });
53
+ expect(result.kind).toBe('invalid');
54
+ });
55
+ });
56
+
57
+ test('a menu link opens the dev menu', () => {
58
+ expect(parseDevLink({ link: 'lowdefy-dev://menu', scheme })).toEqual({ kind: 'menu' });
59
+ });
60
+
61
+ test('a connect link without a server url is invalid', () => {
62
+ expect(parseDevLink({ link: 'lowdefy-dev://connect?url=ftp%3A%2F%2Fx', scheme })).toEqual({
63
+ kind: 'invalid',
64
+ reason: 'The link has no dev server url. Received "ftp://x".',
65
+ });
66
+ });
67
+
68
+ test('links that are not Lowdefy Dev links are left to the router', () => {
69
+ expect(parseDevLink({ link: 'lowdefy-dev://tickets/7', scheme })).toBeNull();
70
+ expect(parseDevLink({ link: 'field://connect?url=http%3A%2F%2Fx', scheme })).toBeNull();
71
+ expect(parseDevLink({ link: '/tickets/7', scheme })).toBeNull();
72
+ });
73
+
74
+ test('the connect screen reads a typed address or a pasted link', () => {
75
+ expect(parseServerInput({ input: ' 192.168.1.20:3000 ', scheme })).toEqual({
76
+ url: 'http://192.168.1.20:3000',
77
+ host: '192.168.1.20:3000',
78
+ pair: null,
79
+ });
80
+ expect(
81
+ parseServerInput({
82
+ input: 'lowdefy-dev://connect?url=http%3A%2F%2F192.168.1.20%3A3000&pair=k3x9q',
83
+ scheme,
84
+ })
85
+ ).toEqual({ url: 'http://192.168.1.20:3000', host: '192.168.1.20:3000', pair: 'k3x9q' });
86
+ expect(parseServerInput({ input: 'not a server', scheme }).error).toMatch(/Enter the address/);
87
+ });
88
+
89
+ test.each([
90
+ ['localhost:3000', true],
91
+ ['127.0.0.1:3000', true],
92
+ ['[::1]:3000', true],
93
+ ['192.168.1.20:3000', false],
94
+ ['localhost.example.com', false],
95
+ ])('isLoopbackHost(%s) is %s', (host, expected) => {
96
+ expect(isLoopbackHost(host)).toBe(expected);
97
+ });
@@ -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
+ // A link's query string as an object of decoded values. Read by hand: the link arrives before any
18
+ // URL polyfill can be relied on, and it holds only a url and a pairing token.
19
+ function parseQuery(query) {
20
+ const values = {};
21
+ (query ?? '')
22
+ .split('&')
23
+ .filter((part) => part.length > 0)
24
+ .forEach((part) => {
25
+ const index = part.indexOf('=');
26
+ const name = index === -1 ? part : part.slice(0, index);
27
+ const value = index === -1 ? '' : part.slice(index + 1);
28
+ values[decodeURIComponent(name)] = decodeURIComponent(value.replace(/\+/g, ' '));
29
+ });
30
+ return values;
31
+ }
32
+
33
+ export default parseQuery;