@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,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
+ // A path inside the app: one leading slash. "//evil.com/x" and "/\evil.com/x" are links to another
20
+ // host (the router opens them as external URLs), and a control character can hide either (a URL
21
+ // parser drops tabs and newlines). A scheme cannot start a path that starts with "/".
22
+ function isAppPath(path) {
23
+ return (
24
+ type.isString(path) &&
25
+ path.startsWith('/') &&
26
+ !/^\/[/\\]/.test(path) &&
27
+ // eslint-disable-next-line no-control-regex
28
+ !/[\u0000-\u001f]/.test(path)
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Opens one of the app's pages by path, as a notification tap, a shortcut, a widget or a share
34
+ * does: "/tasks/42?tab=notes", with a query object merged into the path's own. Anything that is not
35
+ * a path inside the app is ignored.
36
+ *
37
+ * @param {Object} params
38
+ * @param {Object} params.app The open app (currentApp.js).
39
+ * @param {string} params.path
40
+ * @param {Object} [params.query]
41
+ */
42
+ function openPath({ app, path, query }) {
43
+ if (!isAppPath(path)) {
44
+ return;
45
+ }
46
+ const [pathname, search = ''] = path.split('?');
47
+ const params = new URLSearchParams(search);
48
+ Object.entries(query ?? {}).forEach(([key, value]) => {
49
+ params.set(key, type.isString(value) ? value : JSON.stringify(value));
50
+ });
51
+ const queryString = params.toString();
52
+ app.router.push({ pathname, query: queryString === '' ? undefined : queryString });
53
+ }
54
+
55
+ export default openPath;
@@ -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 openPath from './openPath.js';
18
+
19
+ function open(path, query) {
20
+ const app = { router: { push: jest.fn() } };
21
+ openPath({ app, path, query });
22
+ return app.router.push;
23
+ }
24
+
25
+ test('openPath opens a page of the app with its query and the query passed', () => {
26
+ expect(open('/tasks/42?tab=notes', { shared: { n: 1 } })).toHaveBeenCalledWith({
27
+ pathname: '/tasks/42',
28
+ query: 'tab=notes&shared=%7B%22n%22%3A1%7D',
29
+ });
30
+ expect(open('/tasks')).toHaveBeenCalledWith({ pathname: '/tasks', query: undefined });
31
+ });
32
+
33
+ test('openPath ignores anything that is not a path inside the app', () => {
34
+ ['//evil.com/x', '/\\evil.com/x', '/\t/evil.com', 'https://evil.com', 'tasks/1', null].forEach(
35
+ (path) => {
36
+ expect(open(path)).not.toHaveBeenCalled();
37
+ }
38
+ );
39
+ });
@@ -0,0 +1,38 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * A link to the shell's own scheme that the shell handles itself, not the router:
19
+ * - `<scheme>://shortcut/<id>`, from the App Shortcuts intent: { kind: 'shortcut', id };
20
+ * - `<scheme>://expo-sharing`, from the share extension: { kind: 'share' }.
21
+ * null for any other link.
22
+ *
23
+ * @param {string} url
24
+ */
25
+ function parseShellLink(url) {
26
+ const match = /^[a-z][a-z0-9+.-]*:\/\/(shortcut\/([^/?#]+)|expo-sharing)(?:[/?#]|$)/.exec(
27
+ url ?? ''
28
+ );
29
+ if (match === null) {
30
+ return null;
31
+ }
32
+ if (match[2] !== undefined) {
33
+ return { kind: 'shortcut', id: decodeURIComponent(match[2]) };
34
+ }
35
+ return { kind: 'share' };
36
+ }
37
+
38
+ export default parseShellLink;
@@ -0,0 +1,29 @@
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 parseShellLink from './parseShellLink.js';
18
+
19
+ test('parseShellLink reads a shortcut link and the share extension link', () => {
20
+ expect(parseShellLink('acme://shortcut/new-job')).toEqual({ kind: 'shortcut', id: 'new-job' });
21
+ expect(parseShellLink('acme://expo-sharing')).toEqual({ kind: 'share' });
22
+ expect(parseShellLink('acme://expo-sharing?x=1')).toEqual({ kind: 'share' });
23
+ });
24
+
25
+ test('parseShellLink leaves page links and other apps to the router', () => {
26
+ expect(parseShellLink('acme://tasks/42')).toBe(null);
27
+ expect(parseShellLink('https://field.example.com/tasks/42')).toBe(null);
28
+ expect(parseShellLink(null)).toBe(null);
29
+ });
@@ -0,0 +1,94 @@
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 { AppState } from 'react-native';
18
+ import { getDeviceFeature, hasDeviceFeature } from '@lowdefy/blocks-native/device';
19
+
20
+ import refreshWidgets from '../widgets/refreshWidgets.js';
21
+ import { setCurrentApp } from './currentApp.js';
22
+ import startBackgroundFetch from './backgroundFetch.js';
23
+ import startNotifications from '../notifications/startNotifications.js';
24
+ import startShareIn from './startShareIn.js';
25
+ import startShortcuts from './startShortcuts.js';
26
+ import whenAppReady from './whenAppReady.js';
27
+
28
+ /**
29
+ * Starts the native work of the open app once its auth and session exist: notifications, quick
30
+ * actions and App Shortcuts, share-in and background fetch listen at once, and their launch work
31
+ * waits on `app.ready` (whenAppReady: the token read and a root held). Once ready, the app becomes
32
+ * the current app (currentApp.js) for work started outside React, the offline request queue starts
33
+ * and the widgets refresh. When the app goes to the background, a scan screen's brightness goes
34
+ * back to the system.
35
+ *
36
+ * @param {Object} params
37
+ * @param {Object} params.app { apiBase, auth, host, requestQueue, session, shell, triggerAppEvent }.
38
+ * @returns {{ app: Object, shareIn: Object, stop: () => void }}
39
+ */
40
+ function startAppServices({ app: opened }) {
41
+ const ready = whenAppReady({ auth: opened.auth, session: opened.session });
42
+ const app = {
43
+ apiBase: opened.apiBase,
44
+ auth: opened.auth,
45
+ fetch: opened.auth.fetch,
46
+ ready: ready.promise,
47
+ requestQueue: opened.requestQueue,
48
+ router: opened.host.router,
49
+ session: opened.session,
50
+ shell: opened.shell,
51
+ tokenStore: opened.auth.tokenStore,
52
+ triggerAppEvent: opened.triggerAppEvent,
53
+ };
54
+ const shareIn = startShareIn({ app });
55
+ // Lowdefy Dev's share injection hands payloads to it (src/dev/inject/injectShare.js).
56
+ app.receiveShare = shareIn.receive;
57
+ let stopped = false;
58
+ const stops = [
59
+ ready.stop,
60
+ startNotifications({ app }),
61
+ startShortcuts({ app }),
62
+ startBackgroundFetch({ app }),
63
+ shareIn.stop,
64
+ ];
65
+ ready.promise.then(() => {
66
+ if (stopped) return;
67
+ setCurrentApp(app);
68
+ stops.push(app.requestQueue.start());
69
+ refreshWidgets().catch(() => {});
70
+ });
71
+ const appState = AppState.addEventListener('change', (state) => {
72
+ if (state === 'active') {
73
+ refreshWidgets().catch(() => {});
74
+ return;
75
+ }
76
+ if (state === 'background' && hasDeviceFeature('brightness')) {
77
+ getDeviceFeature({ feature: 'brightness', action: 'background' })
78
+ .set({ reset: true })
79
+ .catch(() => {});
80
+ }
81
+ });
82
+ stops.push(() => appState.remove());
83
+ return {
84
+ app,
85
+ shareIn,
86
+ stop: () => {
87
+ stopped = true;
88
+ setCurrentApp(null);
89
+ stops.forEach((stop) => stop());
90
+ },
91
+ };
92
+ }
93
+
94
+ export default startAppServices;
@@ -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 { peekCurrentApp } from './currentApp.js';
18
+ import startAppServices from './startAppServices.js';
19
+ import whenAppReady from './whenAppReady.js';
20
+
21
+ function createStore(initial) {
22
+ let state = initial;
23
+ const listeners = new Set();
24
+ return {
25
+ getState: () => state,
26
+ set(update) {
27
+ state = { ...state, ...update };
28
+ listeners.forEach((listener) => listener());
29
+ },
30
+ subscribe(listener) {
31
+ listeners.add(listener);
32
+ return () => listeners.delete(listener);
33
+ },
34
+ };
35
+ }
36
+
37
+ function createOpened() {
38
+ const auth = createStore({ status: 'starting' });
39
+ auth.fetch = jest.fn();
40
+ auth.tokenStore = { get: () => null, subscribe: () => () => undefined };
41
+ const session = createStore({ root: null });
42
+ return {
43
+ apiBase: 'https://app.test',
44
+ auth,
45
+ host: { router: {} },
46
+ requestQueue: { start: jest.fn(() => () => undefined) },
47
+ session,
48
+ triggerAppEvent: jest.fn(),
49
+ };
50
+ }
51
+
52
+ test('an app is ready once its auth is open and its session holds a root', async () => {
53
+ const { auth, session } = createOpened();
54
+ const ready = whenAppReady({ auth, session });
55
+ const settled = jest.fn();
56
+ ready.promise.then(settled);
57
+ auth.set({ status: 'locked' });
58
+ session.set({ root: {} });
59
+ await Promise.resolve();
60
+ expect(settled).not.toHaveBeenCalled();
61
+ auth.set({ status: 'open' });
62
+ await ready.promise;
63
+ expect(settled).toHaveBeenCalled();
64
+ });
65
+
66
+ test('an app that is already open and holds a root is ready at once', async () => {
67
+ const { auth, session } = createOpened();
68
+ auth.set({ status: 'open' });
69
+ session.set({ root: {} });
70
+ await expect(whenAppReady({ auth, session }).promise).resolves.toBeUndefined();
71
+ });
72
+
73
+ test('the current app and the request queue wait until the app is ready', async () => {
74
+ const opened = createOpened();
75
+ const services = startAppServices({ app: opened });
76
+ await Promise.resolve();
77
+ expect(peekCurrentApp()).toBeNull();
78
+ expect(opened.requestQueue.start).not.toHaveBeenCalled();
79
+ opened.auth.set({ status: 'open' });
80
+ opened.session.set({ root: {} });
81
+ await services.app.ready;
82
+ expect(peekCurrentApp()).toBe(services.app);
83
+ expect(opened.requestQueue.start).toHaveBeenCalledTimes(1);
84
+ services.stop();
85
+ expect(peekCurrentApp()).toBeNull();
86
+ });
87
+
88
+ test('services stopped before the app is ready never make it current', async () => {
89
+ const opened = createOpened();
90
+ const services = startAppServices({ app: opened });
91
+ services.stop();
92
+ opened.auth.set({ status: 'open' });
93
+ opened.session.set({ root: {} });
94
+ await Promise.resolve();
95
+ expect(peekCurrentApp()).toBeNull();
96
+ expect(opened.requestQueue.start).not.toHaveBeenCalled();
97
+ });
@@ -0,0 +1,59 @@
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 { Linking } from 'react-native';
18
+ import * as Sharing from 'expo-sharing';
19
+
20
+ import getPagePath from './getPagePath.js';
21
+ import openPath from './openPath.js';
22
+ import parseShellLink from './parseShellLink.js';
23
+ import toSharePayload from './toSharePayload.js';
24
+
25
+ /**
26
+ * Items shared into the app (the share extension opens `<scheme>://expo-sharing`): opens
27
+ * mobile.share.pageId with the payload in its query as `shared`, and fires onShare with it. A shell
28
+ * without the share extension never gets the link.
29
+ *
30
+ * @param {Object} params
31
+ * @param {Object} params.app The open app (currentApp.js).
32
+ * @returns {{ receive: () => Promise<void>, stop: () => void }} receive takes what is waiting now,
33
+ * or the payloads Lowdefy Dev's share injection passes; stop stops listening.
34
+ */
35
+ function startShareIn({ app }) {
36
+ async function receive({ payloads: injected } = {}) {
37
+ const payloads = injected ?? (await Sharing.getResolvedSharedPayloadsAsync());
38
+ if (injected === undefined) Sharing.clearSharedPayloads();
39
+ if (payloads.length === 0) return;
40
+ // The share page's path comes from the root.
41
+ await app.ready;
42
+ const shared = toSharePayload(payloads);
43
+ const { root } = app.session.getState();
44
+ if (root?.share !== null && root?.share !== undefined) {
45
+ openPath({ app, path: getPagePath({ pageId: root.share.pageId, root }), query: { shared } });
46
+ }
47
+ app.triggerAppEvent({ name: 'onShare', event: shared });
48
+ }
49
+ function onUrl(url) {
50
+ if (parseShellLink(url)?.kind === 'share') receive().catch(() => {});
51
+ }
52
+ const urls = Linking.addEventListener('url', ({ url }) => onUrl(url));
53
+ Linking.getInitialURL()
54
+ .then(onUrl)
55
+ .catch(() => {});
56
+ return { receive, stop: () => urls.remove() };
57
+ }
58
+
59
+ export default startShareIn;
@@ -0,0 +1,88 @@
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 { Linking } from 'react-native';
18
+ import * as QuickActions from 'expo-quick-actions';
19
+
20
+ import openPath from './openPath.js';
21
+ import parseShellLink from './parseShellLink.js';
22
+
23
+ // The quick action or shortcut link that launched the app runs once per launch. The services start
24
+ // again after every sign-out, and QuickActions.initial and the initial url stay as they were, so
25
+ // the next person to sign in would be sent to the shortcut's page again.
26
+ let launchHandled = false;
27
+
28
+ /**
29
+ * The app icon's quick actions and App Shortcuts, from mobile.shortcuts: set on the phone whenever
30
+ * the root changes (expo-quick-actions, which the App Shortcuts intent reads too). Choosing one,
31
+ * from the icon or from Siri or Spotlight (`<scheme>://shortcut/<id>`), opens its path and fires
32
+ * onShortcut with { id, title }.
33
+ *
34
+ * @param {Object} params
35
+ * @param {Object} params.app The open app (currentApp.js).
36
+ * @returns {() => void} Stops listening.
37
+ */
38
+ function startShortcuts({ app }) {
39
+ let applied = null;
40
+ function applyRoot() {
41
+ const shortcuts = app.session.getState().root?.shortcuts;
42
+ if (shortcuts === undefined || shortcuts === applied) return;
43
+ applied = shortcuts;
44
+ QuickActions.setItems(
45
+ shortcuts.map((shortcut) => ({
46
+ id: shortcut.id,
47
+ title: shortcut.title,
48
+ subtitle: shortcut.subtitle,
49
+ icon: shortcut.icon === null ? null : `symbol:${shortcut.icon}`,
50
+ }))
51
+ ).catch(() => {});
52
+ }
53
+ // A quick action that launched the app waits for the root that names its path.
54
+ async function run(id) {
55
+ await app.ready;
56
+ const shortcut = (app.session.getState().root?.shortcuts ?? []).find((item) => item.id === id);
57
+ if (shortcut === undefined) return;
58
+ openPath({ app, path: shortcut.path });
59
+ app.triggerAppEvent({ name: 'onShortcut', event: { id: shortcut.id, title: shortcut.title } });
60
+ }
61
+ function onUrl(url) {
62
+ const link = parseShellLink(url);
63
+ if (link?.kind === 'shortcut') run(link.id).catch(() => {});
64
+ }
65
+
66
+ const unsubscribe = app.session.subscribe(applyRoot);
67
+ applyRoot();
68
+ const quickAction = QuickActions.addListener((action) => {
69
+ run(action.id).catch(() => {});
70
+ });
71
+ const urls = Linking.addEventListener('url', ({ url }) => onUrl(url));
72
+ if (!launchHandled) {
73
+ launchHandled = true;
74
+ if (QuickActions.initial !== undefined) {
75
+ run(QuickActions.initial.id).catch(() => {});
76
+ }
77
+ Linking.getInitialURL()
78
+ .then(onUrl)
79
+ .catch(() => {});
80
+ }
81
+ return () => {
82
+ unsubscribe();
83
+ quickAction.remove();
84
+ urls.remove();
85
+ };
86
+ }
87
+
88
+ export default startShortcuts;
@@ -0,0 +1,59 @@
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 { Linking } from 'react-native';
18
+
19
+ import openPath from './openPath.js';
20
+ import startShortcuts from './startShortcuts.js';
21
+
22
+ jest.mock('./openPath.js', () => jest.fn());
23
+ // The app was launched from the New job quick action.
24
+ jest.mock('expo-quick-actions', () => ({
25
+ initial: { id: 'new_job' },
26
+ setItems: async () => undefined,
27
+ addListener: () => ({ remove: () => undefined }),
28
+ }));
29
+
30
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
31
+
32
+ function createApp() {
33
+ const root = { shortcuts: [{ id: 'new_job', title: 'New job', path: '/jobs/new', icon: null }] };
34
+ return {
35
+ ready: Promise.resolve(),
36
+ session: { getState: () => ({ root }), subscribe: () => () => undefined },
37
+ triggerAppEvent: jest.fn(),
38
+ };
39
+ }
40
+
41
+ test('the quick action that launched the app runs once, not again after a sign-out', async () => {
42
+ jest.spyOn(Linking, 'getInitialURL').mockResolvedValue(null);
43
+ const first = createApp();
44
+ const stop = startShortcuts({ app: first });
45
+ await flush();
46
+ expect(openPath).toHaveBeenCalledWith({ app: first, path: '/jobs/new' });
47
+ expect(first.triggerAppEvent).toHaveBeenCalledWith({
48
+ name: 'onShortcut',
49
+ event: { id: 'new_job', title: 'New job' },
50
+ });
51
+ stop();
52
+
53
+ const second = createApp();
54
+ startShortcuts({ app: second });
55
+ await flush();
56
+ expect(openPath).toHaveBeenCalledTimes(1);
57
+ expect(second.triggerAppEvent).not.toHaveBeenCalled();
58
+ expect(Linking.getInitialURL).toHaveBeenCalledTimes(1);
59
+ });
@@ -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
+ /**
18
+ * What was shared into the app, from expo-sharing's resolved payloads: { text, urls, files }, files
19
+ * as [{ name, mimeType, size, uri, type }] (type: image, video, audio or file).
20
+ *
21
+ * @param {Object[]} payloads expo-sharing ResolvedSharePayloads.
22
+ */
23
+ function toSharePayload(payloads) {
24
+ const texts = [];
25
+ const urls = [];
26
+ const files = [];
27
+ payloads.forEach((payload) => {
28
+ if (payload.shareType === 'text') {
29
+ texts.push(payload.value);
30
+ return;
31
+ }
32
+ if (payload.shareType === 'url' || payload.contentType === 'website') {
33
+ urls.push(payload.value);
34
+ return;
35
+ }
36
+ files.push({
37
+ name: payload.originalName ?? payload.value.split('/').pop(),
38
+ mimeType: payload.contentMimeType ?? payload.mimeType ?? null,
39
+ size: payload.contentSize ?? null,
40
+ uri: payload.contentUri ?? payload.value,
41
+ type: payload.contentType ?? payload.shareType,
42
+ });
43
+ });
44
+ return { text: texts.length === 0 ? null : texts.join('\n'), urls, files };
45
+ }
46
+
47
+ export default toSharePayload;
@@ -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 toSharePayload from './toSharePayload.js';
18
+
19
+ test('toSharePayload splits shared text, links and files', () => {
20
+ expect(
21
+ toSharePayload([
22
+ { shareType: 'text', value: 'Pump 4 is leaking' },
23
+ { shareType: 'url', value: 'https://example.com/report' },
24
+ {
25
+ shareType: 'image',
26
+ value: 'file:///group/photo.jpg',
27
+ contentUri: 'file:///group/photo.jpg',
28
+ contentType: 'image',
29
+ contentMimeType: 'image/jpeg',
30
+ originalName: 'photo.jpg',
31
+ contentSize: 2048,
32
+ },
33
+ ])
34
+ ).toEqual({
35
+ text: 'Pump 4 is leaking',
36
+ urls: ['https://example.com/report'],
37
+ files: [
38
+ {
39
+ name: 'photo.jpg',
40
+ mimeType: 'image/jpeg',
41
+ size: 2048,
42
+ uri: 'file:///group/photo.jpg',
43
+ type: 'image',
44
+ },
45
+ ],
46
+ });
47
+ });