@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,39 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import getAuthSettings from './getAuthSettings.js';
18
+
19
+ /**
20
+ * What the app shows for its auth state and session state:
21
+ * - `lock`: the lock screen, before any page;
22
+ * - `loading`: the token is being read;
23
+ * - `signIn`: the app has auth and nobody is signed in;
24
+ * - `app`: the app's pages (or the session's own loading and error screens).
25
+ */
26
+ function getAuthView({ auth, session }) {
27
+ if (auth.status === 'locked') {
28
+ return 'lock';
29
+ }
30
+ if (auth.status === 'starting') {
31
+ return 'loading';
32
+ }
33
+ if (session.status === 'ready' && getAuthSettings(session.root) !== null && !auth.hasToken) {
34
+ return 'signIn';
35
+ }
36
+ return 'app';
37
+ }
38
+
39
+ export default getAuthView;
@@ -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
+ import getAuthView from './getAuthView.js';
18
+ import shouldLock from './shouldLock.js';
19
+
20
+ const settings = { emailCode: true, unlock: 'none', unlockAfter: 300, sharedDevice: false };
21
+ const ready = { status: 'ready', root: { auth: settings } };
22
+
23
+ test('getAuthView shows the lock screen before anything else', () => {
24
+ expect(getAuthView({ auth: { status: 'locked', hasToken: true }, session: ready })).toBe('lock');
25
+ });
26
+
27
+ test('getAuthView waits while the token is read', () => {
28
+ expect(getAuthView({ auth: { status: 'starting', hasToken: false }, session: ready })).toBe(
29
+ 'loading'
30
+ );
31
+ });
32
+
33
+ test('getAuthView asks for sign-in when the app has auth and nobody is signed in', () => {
34
+ expect(getAuthView({ auth: { status: 'open', hasToken: false }, session: ready })).toBe('signIn');
35
+ expect(getAuthView({ auth: { status: 'open', hasToken: true }, session: ready })).toBe('app');
36
+ });
37
+
38
+ test('getAuthView opens an app without auth, and leaves loading and errors to the app', () => {
39
+ const open = { status: 'open', hasToken: false };
40
+ expect(getAuthView({ auth: open, session: { status: 'ready', root: { auth: null } } })).toBe(
41
+ 'app'
42
+ );
43
+ expect(getAuthView({ auth: open, session: { status: 'loading', root: null } })).toBe('app');
44
+ expect(getAuthView({ auth: open, session: { status: 'error', root: null } })).toBe('app');
45
+ });
46
+
47
+ test('shouldLock locks after unlockAfter seconds in the background, not before', () => {
48
+ expect(shouldLock({ backgroundedAt: null, now: 1e9, unlockAfter: 0 })).toBe(false);
49
+ expect(shouldLock({ backgroundedAt: 1000, now: 300_999, unlockAfter: 300 })).toBe(false);
50
+ expect(shouldLock({ backgroundedAt: 1000, now: 301_000, unlockAfter: 300 })).toBe(true);
51
+ expect(shouldLock({ backgroundedAt: 1000, now: 1000, unlockAfter: 0 })).toBe(true);
52
+ });
@@ -0,0 +1,35 @@
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
+ * Where the sign-in sheet returns to the shell with its one-time token: on iOS the shell's scheme,
19
+ * <scheme>://auth, which ASWebAuthenticationSession hands only to the app that opened the sheet;
20
+ * on Android the app's https app link, <app url>/auth/mobile/callback, which Android opens only in
21
+ * the shell the app's assetlinks.json names (app.config.ts declares it, authCallbackIntentFilters).
22
+ *
23
+ * @param {Object} params
24
+ * @param {string} params.apiBase The app server's URL.
25
+ * @param {'ios'|'android'} params.platform
26
+ * @param {{ scheme }} params.shell The shell config.
27
+ */
28
+ function getSheetReturnUrl({ apiBase, platform, shell }) {
29
+ if (platform === 'android') {
30
+ return `${apiBase.replace(/\/$/, '')}/auth/mobile/callback`;
31
+ }
32
+ return `${shell.scheme}://auth`;
33
+ }
34
+
35
+ export default getSheetReturnUrl;
@@ -0,0 +1,46 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import * as SecureStore from 'expo-secure-store';
18
+
19
+ /**
20
+ * How the session token is kept in the Keychain or Keystore: on this device only, never in a
21
+ * backup.
22
+ *
23
+ * - A shell with background work (location reports while the phone is locked) keeps it readable
24
+ * after the phone's first unlock, so its background task can call the server. Biometrics then
25
+ * guard the screens only.
26
+ * - Any other shell keeps it readable only while the phone is unlocked, and, with biometric
27
+ * unlock, behind Face ID or a fingerprint: reading it is the unlock prompt. A change to the
28
+ * phone's biometrics makes it unreadable, and the person signs in again.
29
+ *
30
+ * A protected item is read and deleted with the same requireAuthentication it was written with.
31
+ */
32
+ function getTokenStoreOptions({ background, biometric }) {
33
+ if (background) {
34
+ return { keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY };
35
+ }
36
+ if (biometric) {
37
+ return {
38
+ keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
39
+ requireAuthentication: true,
40
+ authenticationPrompt: 'Unlock to continue',
41
+ };
42
+ }
43
+ return { keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY };
44
+ }
45
+
46
+ export default getTokenStoreOptions;
@@ -0,0 +1,26 @@
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
+ // The shell profile that reports location in the background (shell.json `profile`). Its
18
+ // background task calls the server while the phone is locked, so its session token must stay
19
+ // readable then, and biometrics guard only its screens.
20
+ const BACKGROUND_PROFILES = ['field-tracking'];
21
+
22
+ function isBackgroundShell(shell) {
23
+ return BACKGROUND_PROFILES.includes(shell.profile);
24
+ }
25
+
26
+ export default isBackgroundShell;
@@ -0,0 +1,35 @@
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
+ // The link without its query or fragment. React Native's URL has no working pathname, so the
18
+ // link is cut by hand, as parseQuery reads its query.
19
+ function stripQuery(link) {
20
+ const end = link.search(/[?#]/);
21
+ return end === -1 ? link : link.slice(0, end);
22
+ }
23
+
24
+ /**
25
+ * Whether a link the router is opening is the sign-in sheet's return: the Android app link
26
+ * <app url>/auth/mobile/callback (getSheetReturnUrl). signInWithSheet reads it from the sheet's
27
+ * result, so the router must not open it as a page: its query holds the one-time token.
28
+ *
29
+ * @param {string} link The URL, or the path, Expo Router received.
30
+ */
31
+ function isSheetReturnLink(link) {
32
+ return /^(https:\/\/[^/]+)?(\/.*)?\/auth\/mobile\/callback\/?$/.test(stripQuery(link));
33
+ }
34
+
35
+ export default isSheetReturnLink;
@@ -0,0 +1,39 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // The query of a URL as { name: value }. React Native's URL has no working searchParams, and the
18
+ // sheet's redirect URL is a custom scheme.
19
+ function parseQuery(url) {
20
+ const start = url.indexOf('?');
21
+ if (start === -1) {
22
+ return {};
23
+ }
24
+ const end = url.indexOf('#', start);
25
+ const query = url.slice(start + 1, end === -1 ? undefined : end);
26
+ const params = {};
27
+ query
28
+ .split('&')
29
+ .filter((part) => part.length > 0)
30
+ .forEach((part) => {
31
+ const separator = part.indexOf('=');
32
+ const name = separator === -1 ? part : part.slice(0, separator);
33
+ const value = separator === -1 ? '' : part.slice(separator + 1);
34
+ params[decodeURIComponent(name)] = decodeURIComponent(value.replace(/\+/g, ' '));
35
+ });
36
+ return params;
37
+ }
38
+
39
+ export default parseQuery;
@@ -0,0 +1,80 @@
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 { Image } from 'expo-image';
18
+ import { appStorageKey } from '@lowdefy/helpers';
19
+ import { getDeviceFeature, hasDeviceFeature } from '@lowdefy/blocks-native/device';
20
+
21
+ import getLastCacheKeyName from '../cache/getLastCacheKeyName.js';
22
+
23
+ /**
24
+ * Removes everything the shell keeps for one app's signed-in person: the session token, every
25
+ * cached root and page of the app (all its callers), the last cache key, and the person's keys in
26
+ * the device store (SetLocalStorage's user keys, which the core puts under one prefix per app),
27
+ * the requests the offline queue still held for them, and the phone's work for them: background
28
+ * location and geofences stop and their backlog goes, scheduled notifications are cancelled and
29
+ * the badge cleared (on shells that carry those features), and the images expo-image cached for
30
+ * them, avatar photos and Markdown images that loaded with their token, leave memory and disk.
31
+ * Device preferences stay. Nothing a person saw outlives their sign-out.
32
+ *
33
+ * @param {Object} params
34
+ * @param {string} params.appId The app.
35
+ * @param {Object} params.cacheStore The page cache.
36
+ * @param {{ keys, remove }} params.kv The device store.
37
+ * @param {{ removeApp }} params.queueStore The offline request queue.
38
+ * @param {string|null} params.slug The app's slug (lowdefyApp.slug), which names its storage keys.
39
+ * @param {{ clear }} params.tokenStore The session token store.
40
+ */
41
+ // The tracking feature's keys (src/device/tracking): the device store holds one tracking config and
42
+ // backlog for the shell, not one per person.
43
+ const TRACKING_KEYS = ['lowdefy-shell:tracking', 'lowdefy-shell:tracking-backlog'];
44
+
45
+ // Calls a device feature the shell may not carry; a feature that fails does not stop the purge.
46
+ async function callFeature(feature, call) {
47
+ if (!hasDeviceFeature(feature)) return;
48
+ try {
49
+ await call(getDeviceFeature({ feature, action: 'SignOut' }));
50
+ } catch (error) {
51
+ // The rest of the purge still runs.
52
+ }
53
+ }
54
+
55
+ async function purgeAppData({ appId, cacheStore, kv, queueStore, slug, tokenStore }) {
56
+ await callFeature('tracking', (tracking) => tracking.stopTracking());
57
+ await callFeature('tracking', (tracking) => tracking.stopGeofencing());
58
+ TRACKING_KEYS.forEach((key) => kv.remove(key));
59
+ await callFeature('notifications', async (notifications) => {
60
+ await notifications.cancel({ all: true });
61
+ await notifications.setBadgeCount({ count: 0 });
62
+ });
63
+ await tokenStore.clear();
64
+ await cacheStore.removeApp({ appId });
65
+ await queueStore.removeApp({ appId });
66
+ kv.remove(getLastCacheKeyName(appId));
67
+ // The shell renders every page with basePath '' (PageScreen).
68
+ const userPrefix = appStorageKey({ basePath: '', name: 'lowdefy_user:', slug });
69
+ kv.keys()
70
+ .filter((key) => key.startsWith(userPrefix))
71
+ .forEach((key) => kv.remove(key));
72
+ try {
73
+ await Promise.all([Image.clearMemoryCache(), Image.clearDiskCache()]);
74
+ } catch (error) {
75
+ // The session and its data are already gone; an image cache that fails to clear is not worth
76
+ // failing the sign-out for.
77
+ }
78
+ }
79
+
80
+ export default purgeAppData;
@@ -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 { Image } from 'expo-image';
18
+ import { installDevice } from '@lowdefy/blocks-native/device';
19
+
20
+ import purgeAppData from './purgeAppData.js';
21
+
22
+ function memoryKv(entries) {
23
+ const values = new Map(Object.entries(entries));
24
+ return {
25
+ keys: () => [...values.keys()],
26
+ remove: (key) => values.delete(key),
27
+ values,
28
+ };
29
+ }
30
+
31
+ function purge(kv) {
32
+ return purgeAppData({
33
+ appId: 'app',
34
+ cacheStore: { removeApp: jest.fn() },
35
+ kv,
36
+ queueStore: { removeApp: jest.fn() },
37
+ slug: null,
38
+ tokenStore: { clear: jest.fn() },
39
+ });
40
+ }
41
+
42
+ test('a sign-out stops tracking and geofencing, drops the backlog and clears notifications', async () => {
43
+ const tracking = { stopTracking: jest.fn(), stopGeofencing: jest.fn() };
44
+ const notifications = { cancel: jest.fn(), setBadgeCount: jest.fn() };
45
+ const removeDevice = installDevice({ notifications, tracking });
46
+ const kv = memoryKv({
47
+ 'lowdefy-shell:tracking': '{}',
48
+ 'lowdefy-shell:tracking-backlog': '[]',
49
+ 'lowdefy-shell:preferences': '{}',
50
+ });
51
+ await purge(kv);
52
+ removeDevice();
53
+ expect(tracking.stopTracking).toHaveBeenCalled();
54
+ expect(tracking.stopGeofencing).toHaveBeenCalled();
55
+ expect(notifications.cancel).toHaveBeenCalledWith({ all: true });
56
+ expect(notifications.setBadgeCount).toHaveBeenCalledWith({ count: 0 });
57
+ expect([...kv.values.keys()]).toEqual(['lowdefy-shell:preferences']);
58
+ });
59
+
60
+ test('a sign-out clears the images expo-image cached in memory and on disk', async () => {
61
+ const clearMemoryCache = jest.spyOn(Image, 'clearMemoryCache');
62
+ const clearDiskCache = jest.spyOn(Image, 'clearDiskCache');
63
+ await purge(memoryKv({}));
64
+ expect(clearMemoryCache).toHaveBeenCalled();
65
+ expect(clearDiskCache).toHaveBeenCalled();
66
+ clearMemoryCache.mockRestore();
67
+ clearDiskCache.mockRestore();
68
+ });
69
+
70
+ test('an image cache that fails to clear does not fail the sign-out', async () => {
71
+ const clearDiskCache = jest
72
+ .spyOn(Image, 'clearDiskCache')
73
+ .mockRejectedValue(new Error('disk cache busy'));
74
+ const kv = memoryKv({ 'lowdefy-shell:tracking-backlog': '[]' });
75
+ await expect(purge(kv)).resolves.toBeUndefined();
76
+ expect([...kv.values.keys()]).toEqual([]);
77
+ clearDiskCache.mockRestore();
78
+ });
79
+
80
+ test('a sign-out on a shell without tracking or notifications still purges', async () => {
81
+ const kv = memoryKv({ 'lowdefy-shell:tracking-backlog': '[]' });
82
+ await purge(kv);
83
+ expect([...kv.values.keys()]).toEqual([]);
84
+ });
85
+
86
+ test('a feature that fails to stop does not stop the purge', async () => {
87
+ const tracking = {
88
+ stopTracking: jest.fn().mockRejectedValue(new Error('denied')),
89
+ stopGeofencing: jest.fn(),
90
+ };
91
+ const removeDevice = installDevice({ tracking });
92
+ const tokenStore = { clear: jest.fn() };
93
+ await purgeAppData({
94
+ appId: 'app',
95
+ cacheStore: { removeApp: jest.fn() },
96
+ kv: memoryKv({}),
97
+ queueStore: { removeApp: jest.fn() },
98
+ slug: null,
99
+ tokenStore,
100
+ });
101
+ removeDevice();
102
+ expect(tracking.stopGeofencing).toHaveBeenCalled();
103
+ expect(tokenStore.clear).toHaveBeenCalled();
104
+ });
@@ -0,0 +1,26 @@
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
+ // Whether the app locks on coming back: it was in the background for unlockAfter seconds or
18
+ // longer. backgroundedAt is null when it never left.
19
+ function shouldLock({ backgroundedAt, now, unlockAfter }) {
20
+ if (backgroundedAt === null) {
21
+ return false;
22
+ }
23
+ return now - backgroundedAt >= unlockAfter * 1000;
24
+ }
25
+
26
+ export default shouldLock;
@@ -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 * as Crypto from 'expo-crypto';
18
+ import * as WebBrowser from 'expo-web-browser';
19
+ import { Platform } from 'react-native';
20
+ import { createPkceChallenge } from '@lowdefy/client-core';
21
+
22
+ import getSheetReturnUrl from './getSheetReturnUrl.js';
23
+ import parseQuery from './parseQuery.js';
24
+
25
+ const VERIFIER_BYTES = 64;
26
+
27
+ function sha256(data) {
28
+ return Crypto.digest(Crypto.CryptoDigestAlgorithm.SHA256, data);
29
+ }
30
+
31
+ /**
32
+ * Signs in through the app's own sign-in page in a system sheet: every method the web app offers
33
+ * (passkeys, SSO, magic links, a second factor).
34
+ *
35
+ * - The sheet opens /auth/mobile with a random state, a PKCE S256 challenge and the platform. On
36
+ * iOS it is ephemeral (no cookies shared with Safari).
37
+ * - Once the person is signed in, the page asks them to confirm. On Continue the server returns a
38
+ * one-time token for a new phone session, bound to the challenge: on iOS to
39
+ * <scheme>://auth?token=&state=, which ASWebAuthenticationSession hands only to this app; on
40
+ * Android to the verified app link <app url>/auth/mobile/callback?token=&state=, which Android
41
+ * opens only in this shell (getSheetReturnUrl).
42
+ * - The shell checks state is its own, then exchanges the token with the code verifier for the
43
+ * session token. A return with another state is refused, so a link this sign-in did not start is
44
+ * never exchanged. The verifier lives in this call only: it is in no URL and never stored.
45
+ *
46
+ * Returns { status }: `signedIn`, `cancelled` (the person closed the sheet or pressed Cancel) or
47
+ * `failed`.
48
+ *
49
+ * @param {Object} params
50
+ * @param {string} params.apiBase The app server's URL.
51
+ * @param {Object} params.authApi createAuthApi.
52
+ * @param {{ id, scheme }} params.shell The shell config.
53
+ */
54
+ async function signInWithSheet({ apiBase, authApi, shell }) {
55
+ const state = Crypto.randomUUID();
56
+ const { codeVerifier, codeChallenge, codeChallengeMethod } = await createPkceChallenge({
57
+ verifierBytes: Crypto.getRandomBytes(VERIFIER_BYTES),
58
+ sha256,
59
+ });
60
+ const platform = Platform.OS === 'android' ? 'android' : 'ios';
61
+ const query = [
62
+ ['shell', shell.id],
63
+ ['state', state],
64
+ ['code_challenge', codeChallenge],
65
+ ['code_challenge_method', codeChallengeMethod],
66
+ ['platform', platform],
67
+ ]
68
+ .map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
69
+ .join('&');
70
+ const returnUrl = getSheetReturnUrl({ apiBase, platform, shell });
71
+ const result = await WebBrowser.openAuthSessionAsync(
72
+ `${apiBase}/auth/mobile?${query}`,
73
+ returnUrl,
74
+ {
75
+ preferEphemeralSession: true,
76
+ }
77
+ );
78
+ if (result.type !== 'success') {
79
+ return { status: 'cancelled' };
80
+ }
81
+ if (!result.url.startsWith(returnUrl)) {
82
+ return { status: 'failed' };
83
+ }
84
+ const params = parseQuery(result.url);
85
+ if (params.state !== state) {
86
+ return { status: 'failed' };
87
+ }
88
+ if (params.error === 'cancelled') {
89
+ return { status: 'cancelled' };
90
+ }
91
+ if (!params.token) {
92
+ return { status: 'failed' };
93
+ }
94
+ return authApi.exchangeToken({ token: params.token, codeVerifier });
95
+ }
96
+
97
+ export default signInWithSheet;