@lowdefy/shell 3.23.2 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. package/LICENSE +183 -183
  2. package/README.md +364 -2
  3. package/app/(t0,t1,t2,t3,t4)/[...path].js +38 -0
  4. package/app/(t0,t1,t2,t3,t4)/_layout.js +48 -0
  5. package/app/(t0,t1,t2,t3,t4)/index.js +38 -0
  6. package/app/+native-intent.js +39 -0
  7. package/app/_layout.js +36 -0
  8. package/app.config.ts +183 -0
  9. package/appLinks.js +65 -0
  10. package/applyShellPlugins.js +43 -0
  11. package/authCallbackIntentFilters.js +46 -0
  12. package/babel.config.js +25 -0
  13. package/checkShellConfig.js +172 -0
  14. package/checkShellPlugins.js +74 -0
  15. package/getPrivacyManifest.js +39 -0
  16. package/metro.config.js +73 -0
  17. package/native/LowdefyAppShortcuts.swift.template +84 -0
  18. package/native/withAppShortcuts.js +60 -0
  19. package/native-fingerprint.json +16 -0
  20. package/package.json +120 -40
  21. package/profiles.js +607 -0
  22. package/readShellConfig.js +36 -0
  23. package/resolveShellPlugins.js +109 -0
  24. package/runtimeVersion.js +112 -0
  25. package/scripts/checkReleaseBundle.mjs +358 -0
  26. package/scripts/createDayjsLocales.mjs +67 -0
  27. package/scripts/createShellAssets.mjs +54 -0
  28. package/scripts/createShellVersion.mjs +83 -0
  29. package/scripts/createTypeManifest.mjs +205 -0
  30. package/shell.dev.json +11 -0
  31. package/shellVariant.js +126 -0
  32. package/src/ShellContext.js +32 -0
  33. package/src/actions/CopyToClipboard.js +29 -0
  34. package/src/actions/CopyToClipboard.test.js +33 -0
  35. package/src/actions/GeolocationCurrentPosition.js +61 -0
  36. package/src/actions/GeolocationCurrentPosition.test.js +79 -0
  37. package/src/actions/GetLocalStorage.js +36 -0
  38. package/src/actions/RemoveLocalStorage.js +31 -0
  39. package/src/actions/ScrollTo.js +66 -0
  40. package/src/actions/ScrollTo.test.js +83 -0
  41. package/src/actions/SetFocus.js +33 -0
  42. package/src/actions/SetFocus.test.js +49 -0
  43. package/src/actions/SetLocalStorage.js +34 -0
  44. package/src/actions/localStorage.test.js +68 -0
  45. package/src/actions/measureInScrollView.js +36 -0
  46. package/src/actions/nativeActions.js +41 -0
  47. package/src/auth/AuthButton.js +44 -0
  48. package/src/auth/AuthGate.js +57 -0
  49. package/src/auth/AuthLink.js +43 -0
  50. package/src/auth/LockScreen.js +67 -0
  51. package/src/auth/LockScreen.test.js +37 -0
  52. package/src/auth/SignInScreen.js +268 -0
  53. package/src/auth/SignInScreen.test.js +187 -0
  54. package/src/auth/createAppAuth.js +325 -0
  55. package/src/auth/createAppAuth.test.js +424 -0
  56. package/src/auth/createAuthApi.js +115 -0
  57. package/src/auth/createAuthApi.test.js +138 -0
  58. package/src/auth/createCoreAuth.js +96 -0
  59. package/src/auth/createCoreAuth.test.js +188 -0
  60. package/src/auth/createDeviceKey.js +62 -0
  61. package/src/auth/createDeviceKey.test.js +54 -0
  62. package/src/auth/createSessionStores.js +45 -0
  63. package/src/auth/createTokenStore.js +103 -0
  64. package/src/auth/createTokenStore.test.js +116 -0
  65. package/src/auth/getAuthSettings.js +24 -0
  66. package/src/auth/getAuthView.js +39 -0
  67. package/src/auth/getAuthView.test.js +52 -0
  68. package/src/auth/getSheetReturnUrl.js +35 -0
  69. package/src/auth/getTokenStoreOptions.js +46 -0
  70. package/src/auth/isBackgroundShell.js +26 -0
  71. package/src/auth/isSheetReturnLink.js +35 -0
  72. package/src/auth/parseQuery.js +39 -0
  73. package/src/auth/purgeAppData.js +80 -0
  74. package/src/auth/purgeAppData.test.js +104 -0
  75. package/src/auth/shouldLock.js +26 -0
  76. package/src/auth/signInWithSheet.js +97 -0
  77. package/src/auth/signInWithSheet.test.js +205 -0
  78. package/src/auth/styles.js +78 -0
  79. package/src/auth/unregisterPushToken.js +62 -0
  80. package/src/auth/unregisterPushToken.test.js +67 -0
  81. package/src/auth/usesBiometricUnlock.js +23 -0
  82. package/src/auth/watchSession.js +47 -0
  83. package/src/auth/withoutCookies.js +26 -0
  84. package/src/cache/createSqliteCacheStore.js +113 -0
  85. package/src/cache/createSqliteCacheStore.test.js +93 -0
  86. package/src/cache/getCacheKey.js +25 -0
  87. package/src/cache/getLastCacheKeyName.js +22 -0
  88. package/src/components/AppProvider.js +118 -0
  89. package/src/components/Frame.js +116 -0
  90. package/src/components/Frame.test.js +101 -0
  91. package/src/components/LoadingScreen.js +35 -0
  92. package/src/components/MenuButton.js +121 -0
  93. package/src/components/MenuButton.test.js +74 -0
  94. package/src/components/MessageScreen.js +51 -0
  95. package/src/components/NavigationTheme.js +33 -0
  96. package/src/components/NoConnectionBanner.js +51 -0
  97. package/src/components/NoConnectionBanner.test.js +73 -0
  98. package/src/components/OtherPageScreen.js +95 -0
  99. package/src/components/OtherPageScreen.test.js +74 -0
  100. package/src/components/PageScreen.js +254 -0
  101. package/src/components/PageScreen.test.js +492 -0
  102. package/src/components/RouteErrorScreen.js +47 -0
  103. package/src/components/ShellRoot.js +34 -0
  104. package/src/components/ShellRoot.test.js +62 -0
  105. package/src/components/ShellThemeProvider.js +45 -0
  106. package/src/components/SwitchToTab.js +36 -0
  107. package/src/components/SwitchToTab.test.js +63 -0
  108. package/src/components/TabIconImages.js +72 -0
  109. package/src/components/UpdateScreen.js +40 -0
  110. package/src/components/getNavigationTheme.js +44 -0
  111. package/src/components/getNavigationTheme.test.js +41 -0
  112. package/src/components/styles.js +50 -0
  113. package/src/components/useSurfaceTokens.js +47 -0
  114. package/src/config/getAppApiBase.js +34 -0
  115. package/src/config/getShellConfig.js +37 -0
  116. package/src/config/getShellConfig.test.js +80 -0
  117. package/src/createShellApp.js +126 -0
  118. package/src/dev/DevShell.js +239 -0
  119. package/src/dev/DevShell.test.js +233 -0
  120. package/src/dev/connect/ConfirmSheet.js +71 -0
  121. package/src/dev/connect/ConnectScreen.js +117 -0
  122. package/src/dev/connect/QrScanner.js +89 -0
  123. package/src/dev/connection/createDevShellConfig.js +47 -0
  124. package/src/dev/connection/createDevTransport.js +51 -0
  125. package/src/dev/connection/createDevTransport.test.js +87 -0
  126. package/src/dev/connection/createServerList.js +48 -0
  127. package/src/dev/connection/isLoopbackHost.js +25 -0
  128. package/src/dev/connection/pairedShellApp.test.js +104 -0
  129. package/src/dev/connection/parseDevLink.js +66 -0
  130. package/src/dev/connection/parseDevLink.test.js +97 -0
  131. package/src/dev/connection/parseQuery.js +33 -0
  132. package/src/dev/connection/parseServerInput.js +40 -0
  133. package/src/dev/connection/parseServerUrl.js +45 -0
  134. package/src/dev/devStyles.js +97 -0
  135. package/src/dev/index.js +21 -0
  136. package/src/dev/inject/createDeviceInjections.js +188 -0
  137. package/src/dev/inject/createDeviceInjections.test.js +224 -0
  138. package/src/dev/inject/createOfflineFetch.js +35 -0
  139. package/src/dev/inject/createOfflineFetch.test.js +68 -0
  140. package/src/dev/inject/injectPush.js +60 -0
  141. package/src/dev/inject/injectShare.js +55 -0
  142. package/src/dev/inject/withInjectedDevice.js +98 -0
  143. package/src/dev/menu/BlockGallery.js +33 -0
  144. package/src/dev/menu/DevMenu.js +121 -0
  145. package/src/dev/menu/createLogBuffer.js +83 -0
  146. package/src/dev/menu/watchShake.js +52 -0
  147. package/src/dev/menu/watchShake.test.js +61 -0
  148. package/src/dev/recorder/createNativeRecorder.js +204 -0
  149. package/src/dev/recorder/createNativeRecorder.test.js +307 -0
  150. package/src/dev/redirectDevLink.js +30 -0
  151. package/src/dev/redirectDevLink.test.js +49 -0
  152. package/src/dev/steps/createStepRunner.js +184 -0
  153. package/src/dev/steps/createStepRunner.test.js +262 -0
  154. package/src/dev/steps/findBlock.js +78 -0
  155. package/src/dev/steps/isContextSettled.js +37 -0
  156. package/src/dev/steps/waitForSettle.js +39 -0
  157. package/src/dev/steps/waitUntil.js +40 -0
  158. package/src/dev/tab/DevTabClient.js +97 -0
  159. package/src/dev/tab/answerEval.js +39 -0
  160. package/src/dev/tab/answerInspect.js +45 -0
  161. package/src/dev/tab/createDevTabClient.js +272 -0
  162. package/src/dev/tab/createDevTabClient.test.js +317 -0
  163. package/src/dev/tab/describeTabError.js +35 -0
  164. package/src/dev/tab/devClientMarker.js +22 -0
  165. package/src/dev/tab/devClientMarker.test.js +23 -0
  166. package/src/dev/tab/findPageContext.js +43 -0
  167. package/src/dev/tab/getShownPage.js +42 -0
  168. package/src/dev/tab/readEventStream.js +68 -0
  169. package/src/dev/tab/reloadApp.js +42 -0
  170. package/src/dev/tab/reloadApp.test.js +115 -0
  171. package/src/device/DeviceHosts.js +32 -0
  172. package/src/device/basic/createFilesFeature.js +41 -0
  173. package/src/device/basic/createHapticsFeature.js +46 -0
  174. package/src/device/basic/createKeepAwakeFeature.js +37 -0
  175. package/src/device/basic/createPhotosFeature.js +40 -0
  176. package/src/device/basic/createRequestQueueFeature.js +37 -0
  177. package/src/device/basic/createShareFeature.js +63 -0
  178. package/src/device/basic/createShareFeature.test.js +47 -0
  179. package/src/device/basic/createSpeechFeature.js +43 -0
  180. package/src/device/basic/createUploadFeature.js +41 -0
  181. package/src/device/basic/toImagePickerOptions.js +41 -0
  182. package/src/device/basic/toPickedFile.js +54 -0
  183. package/src/device/calendar/index.js +68 -0
  184. package/src/device/contacts/index.js +67 -0
  185. package/src/device/createDevice.js +65 -0
  186. package/src/device/device.js +27 -0
  187. package/src/device/field/MapView.js +93 -0
  188. package/src/device/field/ScanCodeSheet.js +86 -0
  189. package/src/device/field/ScannerView.js +52 -0
  190. package/src/device/field/createAudioFeature.js +70 -0
  191. package/src/device/field/createBrightnessFeature.js +40 -0
  192. package/src/device/field/createCameraFeature.js +56 -0
  193. package/src/device/field/createDocumentScannerFeature.js +51 -0
  194. package/src/device/field/createLocationFeature.js +39 -0
  195. package/src/device/field/createScanRequests.js +55 -0
  196. package/src/device/field/index.js +39 -0
  197. package/src/device/field/scanRequests.js +22 -0
  198. package/src/device/injected.js +60 -0
  199. package/src/device/injected.test.js +39 -0
  200. package/src/device/nfc/index.js +89 -0
  201. package/src/device/nfc/toNfcRecord.js +44 -0
  202. package/src/device/nfc/toNfcRecord.test.js +48 -0
  203. package/src/device/tracking/createBacklog.js +63 -0
  204. package/src/device/tracking/createBacklog.test.js +47 -0
  205. package/src/device/tracking/index.js +211 -0
  206. package/src/device/tracking/index.test.js +110 -0
  207. package/src/host/createDeviceStore.js +32 -0
  208. package/src/host/createMemoryStore.js +32 -0
  209. package/src/host/createNetworkStatus.js +94 -0
  210. package/src/host/createNetworkStatus.test.js +94 -0
  211. package/src/host/createRouterAdapter.js +33 -0
  212. package/src/host/createShellFetch.js +62 -0
  213. package/src/host/createShellFetch.test.js +165 -0
  214. package/src/host/createShellGlobals.js +62 -0
  215. package/src/host/createShellHost.js +83 -0
  216. package/src/host/createShellHost.test.js +36 -0
  217. package/src/host/getOrigin.js +31 -0
  218. package/src/host/isNetworkError.js +24 -0
  219. package/src/host/isOnline.js +22 -0
  220. package/src/host/networkStatus.js +24 -0
  221. package/src/host/readServerError.js +45 -0
  222. package/src/host/readServerError.test.js +85 -0
  223. package/src/host/reportShellError.js +47 -0
  224. package/src/host/reportShellError.test.js +79 -0
  225. package/src/host/withNetworkErrors.js +38 -0
  226. package/src/host/withNetworkErrors.test.js +61 -0
  227. package/src/host/withShellHeader.js +25 -0
  228. package/src/host/withShellHeader.test.js +28 -0
  229. package/src/js/evaluateJsEntries.js +43 -0
  230. package/src/js/evaluateJsEntries.test.js +48 -0
  231. package/src/locale/createShellLocale.js +112 -0
  232. package/src/locale/createShellLocale.test.js +74 -0
  233. package/src/locale/dayjsLocales.js +165 -0
  234. package/src/locale/getDeviceLocales.js +23 -0
  235. package/src/locale/loadDayjsLocale.js +32 -0
  236. package/src/locale/resolveActiveLocale.js +46 -0
  237. package/src/locale/resolveActiveLocale.test.js +40 -0
  238. package/src/native/backgroundFetch.js +97 -0
  239. package/src/native/callEndpoint.js +63 -0
  240. package/src/native/createAppEventTrigger.js +70 -0
  241. package/src/native/currentApp.js +49 -0
  242. package/src/native/getPagePath.js +32 -0
  243. package/src/native/openPath.js +55 -0
  244. package/src/native/openPath.test.js +39 -0
  245. package/src/native/parseShellLink.js +38 -0
  246. package/src/native/parseShellLink.test.js +29 -0
  247. package/src/native/startAppServices.js +94 -0
  248. package/src/native/startAppServices.test.js +97 -0
  249. package/src/native/startShareIn.js +59 -0
  250. package/src/native/startShortcuts.js +88 -0
  251. package/src/native/startShortcuts.test.js +59 -0
  252. package/src/native/toSharePayload.js +47 -0
  253. package/src/native/toSharePayload.test.js +47 -0
  254. package/src/native/whenAppReady.js +48 -0
  255. package/src/navigation/SlotContext.js +24 -0
  256. package/src/navigation/frameSlots.js +27 -0
  257. package/src/navigation/getFrame.js +30 -0
  258. package/src/navigation/getInitialScreenOptions.js +63 -0
  259. package/src/navigation/getInitialScreenOptions.test.js +66 -0
  260. package/src/navigation/getPresentation.js +28 -0
  261. package/src/navigation/getSlotRootPath.js +27 -0
  262. package/src/navigation/getStackOptions.js +185 -0
  263. package/src/navigation/getTabHref.js +45 -0
  264. package/src/navigation/getTabIcons.js +43 -0
  265. package/src/navigation/isPageUnderHeader.js +46 -0
  266. package/src/navigation/navigation.test.js +342 -0
  267. package/src/navigation/toAppLinkPath.js +43 -0
  268. package/src/navigation/toAppLinkPath.test.js +53 -0
  269. package/src/navigation/toRouterHref.js +29 -0
  270. package/src/notifications/createNotificationsFeature.js +61 -0
  271. package/src/notifications/getCategoryAction.js +31 -0
  272. package/src/notifications/handleNotificationResponse.js +82 -0
  273. package/src/notifications/registerPushToken.js +71 -0
  274. package/src/notifications/startNotifications.js +114 -0
  275. package/src/notifications/toCategoryActions.js +42 -0
  276. package/src/notifications/toNotificationEvent.js +36 -0
  277. package/src/notifications/toScheduleRequest.js +62 -0
  278. package/src/plugins/appTypes.js +28 -0
  279. package/src/plugins/prefixTypes.js +28 -0
  280. package/src/plugins/shellPlugins.json +15 -0
  281. package/src/plugins/typeManifest.js +789 -0
  282. package/src/plugins/types.js +64 -0
  283. package/src/plugins/types.test.js +173 -0
  284. package/src/polyfills/installGetRandomValues.js +28 -0
  285. package/src/queue/createRequestQueue.js +198 -0
  286. package/src/queue/createRequestQueue.test.js +215 -0
  287. package/src/queue/createSqliteQueueStore.js +121 -0
  288. package/src/queue/summarizeQueue.js +39 -0
  289. package/src/session/createAppSession.js +323 -0
  290. package/src/session/createAppSession.test.js +391 -0
  291. package/src/session/getPageView.js +59 -0
  292. package/src/session/getPageView.test.js +137 -0
  293. package/src/sync/encodePagePath.js +27 -0
  294. package/src/sync/fetchPage.js +72 -0
  295. package/src/sync/fetchPage.test.js +84 -0
  296. package/src/sync/fetchRoot.js +43 -0
  297. package/src/sync/fetchUser.js +33 -0
  298. package/src/sync/runLimited.js +36 -0
  299. package/src/sync/toRootConfig.js +35 -0
  300. package/src/test/createFakeNetwork.js +40 -0
  301. package/src/test/createFakeSecureStore.js +40 -0
  302. package/src/test/createFakeServer.js +98 -0
  303. package/src/test/createMemoryCacheStore.js +59 -0
  304. package/src/test/createNetworkError.js +24 -0
  305. package/src/test/expoImage.js +42 -0
  306. package/src/test/expoNotifications.js +40 -0
  307. package/src/test/expoQuickActions.js +22 -0
  308. package/src/test/expoSpeechRecognition.js +31 -0
  309. package/src/test/expoTaskManager.js +25 -0
  310. package/src/test/expoUi.js +55 -0
  311. package/src/test/expoUiBottomSheet.js +64 -0
  312. package/src/test/expoUiDateTimePicker.js +26 -0
  313. package/src/test/expoUiMenu.js +46 -0
  314. package/src/test/expoUiSegmentedControl.js +47 -0
  315. package/src/test/expoUiSwiftUi.js +62 -0
  316. package/src/test/expoUiSwiftUiModifiers.js +23 -0
  317. package/src/test/expoWebBrowser.js +23 -0
  318. package/src/test/fixtures.js +98 -0
  319. package/src/test/nativeModule.js +33 -0
  320. package/src/test/renderPageScreen.js +109 -0
  321. package/src/types/checkTypeSet.js +39 -0
  322. package/src/types/collectPageTypes.js +49 -0
  323. package/src/types/collectPageTypes.test.js +90 -0
  324. package/src/types/findMissingPageTypes.js +40 -0
  325. package/src/version/compareVersions.js +38 -0
  326. package/src/version/isShellTooOld.js +30 -0
  327. package/src/version/shellVersion.js +21 -0
  328. package/src/version/shellVersion.test.js +32 -0
  329. package/src/widgets/fetchWidgetData.js +59 -0
  330. package/src/widgets/index.js +156 -0
  331. package/src/widgets/refreshWidgets.js +31 -0
  332. package/withExcludedNativeModules.js +93 -0
  333. package/dist/location.js +0 -33
  334. package/dist/public/apple-touch-icon.png +0 -0
  335. package/dist/public/icon-32.png +0 -0
  336. package/dist/public/icon-512.png +0 -0
  337. package/dist/public/icon.svg +0 -17
  338. package/dist/public/logo-dark-theme.png +0 -0
  339. package/dist/public/logo-light-theme.png +0 -0
  340. package/dist/public/logo-square-dark-theme.png +0 -0
  341. package/dist/public/logo-square-light-theme.png +0 -0
  342. package/dist/public/manifest.webmanifest +0 -16
  343. package/dist/shell/246.93432ff68186aae6c87b.js +0 -2
  344. package/dist/shell/246.93432ff68186aae6c87b.js.LICENSE.txt +0 -8
  345. package/dist/shell/454.b597ca21366afda000af.js +0 -2
  346. package/dist/shell/454.b597ca21366afda000af.js.LICENSE.txt +0 -14
  347. package/dist/shell/484.d645e6c09079f45ce044.js +0 -1
  348. package/dist/shell/706.a66931e02a549fd269bd.js +0 -2
  349. package/dist/shell/706.a66931e02a549fd269bd.js.LICENSE.txt +0 -8
  350. package/dist/shell/9.78be8c1b6c3986f404fc.js +0 -2
  351. package/dist/shell/9.78be8c1b6c3986f404fc.js.LICENSE.txt +0 -45
  352. package/dist/shell/922.43b7a3ebe22ef05b3049.js +0 -2
  353. package/dist/shell/922.43b7a3ebe22ef05b3049.js.LICENSE.txt +0 -14
  354. package/dist/shell/index.html +0 -57
  355. package/dist/shell/main.bde87d1433f0625faed6.js +0 -1
  356. package/dist/shell/runtime.1ead93159ba56c05b969.js +0 -1
@@ -0,0 +1,68 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { getCalendars, listEvents, requestCalendarPermissions } from 'expo-calendar';
18
+ import { createEventInCalendarAsync } from 'expo-calendar/legacy';
19
+ import { UserError } from '@lowdefy/errors';
20
+
21
+ const HOUR = 60 * 60 * 1000;
22
+
23
+ /**
24
+ * The calendar device feature (the calendar extension): AddCalendarEvent opens the system event
25
+ * editor filled in, for the person to save; GetCalendarEvents reads events between two dates from
26
+ * every calendar, after asking for calendar access.
27
+ */
28
+ const calendar = {
29
+ async add({ allDay, end, location, notes, start, title, url }) {
30
+ const startDate = new Date(start);
31
+ // The system editor needs no calendar permission; SDK 58 keeps it under expo-calendar/legacy.
32
+ const result = await createEventInCalendarAsync({
33
+ allDay: allDay === true,
34
+ endDate: end ? new Date(end) : new Date(startDate.getTime() + HOUR),
35
+ location,
36
+ notes,
37
+ startDate,
38
+ title,
39
+ url,
40
+ });
41
+ return result.action === 'saved' || result.action === 'done' ? { id: result.id ?? null } : null;
42
+ },
43
+ async getEvents({ end, start }) {
44
+ const { granted } = await requestCalendarPermissions();
45
+ if (!granted) {
46
+ throw new UserError('Calendar permission was denied.');
47
+ }
48
+ const calendars = await getCalendars();
49
+ const byId = Object.fromEntries(calendars.map((item) => [item.id, item.title]));
50
+ const events = await listEvents(calendars, new Date(start), new Date(end));
51
+ return events.map((event) => ({
52
+ id: event.id,
53
+ title: event.title,
54
+ start: new Date(event.startDate).toISOString(),
55
+ end: new Date(event.endDate).toISOString(),
56
+ allDay: event.allDay === true,
57
+ location: event.location ?? null,
58
+ notes: event.notes ?? null,
59
+ calendar: byId[event.calendarId] ?? null,
60
+ }));
61
+ },
62
+ };
63
+
64
+ // The calendar extension's device features (profiles.js CALENDAR_ENTRY).
65
+ const features = { calendar };
66
+ const hosts = [];
67
+
68
+ export { features, hosts };
@@ -0,0 +1,67 @@
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 { Contact, ContactField } from 'expo-contacts';
18
+
19
+ const FIELDS = [
20
+ ContactField.FULL_NAME,
21
+ ContactField.GIVEN_NAME,
22
+ ContactField.FAMILY_NAME,
23
+ ContactField.COMPANY,
24
+ ContactField.EMAILS,
25
+ ContactField.PHONES,
26
+ ];
27
+
28
+ async function toContact(contact) {
29
+ const details = await contact.getDetails(FIELDS);
30
+ return {
31
+ id: contact.id,
32
+ name: details.fullName ?? null,
33
+ firstName: details.givenName ?? null,
34
+ lastName: details.familyName ?? null,
35
+ company: details.company ?? null,
36
+ emails: (details.emails ?? []).map((entry) => entry.address),
37
+ phoneNumbers: (details.phones ?? []).map((entry) => entry.number),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * The contacts device feature (the contacts extension): PickContact opens the system contact
43
+ * picker; AddContact opens the new-contact form filled in for the person to save. Returns
44
+ * { saved } since the form does not say which contact it made.
45
+ */
46
+ const contacts = {
47
+ async pick() {
48
+ const contact = await Contact.presentPicker();
49
+ return contact === null ? null : toContact(contact);
50
+ },
51
+ async add({ company, emails = [], firstName, lastName, phoneNumbers = [] }) {
52
+ const saved = await Contact.presentCreateForm({
53
+ company,
54
+ emails: emails.map((address) => ({ address, label: 'work' })),
55
+ familyName: lastName,
56
+ givenName: firstName,
57
+ phones: phoneNumbers.map((number) => ({ number, label: 'mobile' })),
58
+ });
59
+ return saved ? { saved: true } : null;
60
+ },
61
+ };
62
+
63
+ // The contacts extension's device features (profiles.js CONTACTS_ENTRY).
64
+ const features = { contacts };
65
+ const hosts = [];
66
+
67
+ export { features, hosts };
@@ -0,0 +1,65 @@
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 calendarEntry from './calendar/index.js';
18
+ import * as contactsEntry from './contacts/index.js';
19
+ import createFilesFeature from './basic/createFilesFeature.js';
20
+ import createHapticsFeature from './basic/createHapticsFeature.js';
21
+ import createKeepAwakeFeature from './basic/createKeepAwakeFeature.js';
22
+ import createNotificationsFeature from '../notifications/createNotificationsFeature.js';
23
+ import createPhotosFeature from './basic/createPhotosFeature.js';
24
+ import createRequestQueueFeature from './basic/createRequestQueueFeature.js';
25
+ import createShareFeature from './basic/createShareFeature.js';
26
+ import createSpeechFeature from './basic/createSpeechFeature.js';
27
+ import createUploadFeature from './basic/createUploadFeature.js';
28
+ import * as fieldEntry from './field/index.js';
29
+ import * as nfcEntry from './nfc/index.js';
30
+ import * as trackingEntry from './tracking/index.js';
31
+ import * as widgetsEntry from '../widgets/index.js';
32
+
33
+ // The feature modules of profiles and extensions (profiles.js jsEntries). A shell that does not
34
+ // carry one resolves it to an empty module (metro.config.js), which has no `features`.
35
+ const ENTRIES = [fieldEntry, nfcEntry, trackingEntry, contactsEntry, calendarEntry, widgetsEntry];
36
+
37
+ /**
38
+ * The device features this shell carries, for blocks-native's device registry (installDevice): the
39
+ * ones every shell has, and those of the profile and extensions it was built with. `hosts` are the
40
+ * views features open at the root, such as the ScanCode sheet (DeviceHosts).
41
+ *
42
+ * @returns {{ features: Object, hosts: Function[] }}
43
+ */
44
+ function createDevice() {
45
+ const carried = ENTRIES.filter((entry) => entry.features !== undefined);
46
+ return {
47
+ features: Object.assign(
48
+ {
49
+ files: createFilesFeature(),
50
+ haptics: createHapticsFeature(),
51
+ keepAwake: createKeepAwakeFeature(),
52
+ notifications: createNotificationsFeature(),
53
+ photos: createPhotosFeature(),
54
+ requestQueue: createRequestQueueFeature(),
55
+ share: createShareFeature(),
56
+ speech: createSpeechFeature(),
57
+ upload: createUploadFeature(),
58
+ },
59
+ ...carried.map((entry) => entry.features)
60
+ ),
61
+ hosts: carried.flatMap((entry) => entry.hosts),
62
+ };
63
+ }
64
+
65
+ export default createDevice;
@@ -0,0 +1,27 @@
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 { installDevice } from '@lowdefy/blocks-native/device';
18
+
19
+ import createDevice from './createDevice.js';
20
+
21
+ // The shell's one device: built and installed when the bundle loads, before any page runs an
22
+ // action, so background tasks launched with no screen find it too. Lowdefy Dev installs a wrapped
23
+ // copy that answers injected results first (src/dev/inject/withInjectedDevice.js).
24
+ const device = createDevice();
25
+ installDevice(device.features);
26
+
27
+ export default device;
@@ -0,0 +1,93 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import { Platform } from 'react-native';
19
+ import { AppleMaps, GoogleMaps } from 'expo-maps';
20
+
21
+ const APPLE_MAP_TYPES = { standard: 'STANDARD', satellite: 'IMAGERY', hybrid: 'HYBRID' };
22
+ const GOOGLE_MAP_TYPES = { standard: 'NORMAL', satellite: 'SATELLITE', hybrid: 'HYBRID' };
23
+
24
+ function toCoordinates({ latitude, longitude }) {
25
+ return { latitude, longitude };
26
+ }
27
+
28
+ /**
29
+ * The map the Map block draws (profile field): Apple Maps on iOS, Google Maps on Android
30
+ * (expo-maps), from the block's markers, lines and camera. Taps and camera moves come back in the
31
+ * block's terms.
32
+ */
33
+ function MapView({
34
+ center,
35
+ mapType,
36
+ markerColor,
37
+ markers,
38
+ onMapClick,
39
+ onMarkerClick,
40
+ onRegionChange,
41
+ polylines,
42
+ showsUserLocation,
43
+ style,
44
+ zoom,
45
+ }) {
46
+ const cameraPosition = center === null ? undefined : { coordinates: toCoordinates(center), zoom };
47
+ const mapMarkers = markers.map((marker, index) => ({
48
+ id: marker.id ?? String(index),
49
+ coordinates: toCoordinates(marker),
50
+ title: marker.title,
51
+ snippet: marker.description,
52
+ tintColor: marker.color ?? markerColor,
53
+ }));
54
+ const lines = polylines.map((line, index) => ({
55
+ id: line.id ?? String(index),
56
+ coordinates: line.coordinates.map(toCoordinates),
57
+ color: line.color ?? markerColor,
58
+ width: line.width ?? 4,
59
+ }));
60
+ const events = {
61
+ onMapClick: ({ coordinates }) => onMapClick(coordinates),
62
+ onMarkerClick: (clicked) => {
63
+ const index = mapMarkers.findIndex((marker) => marker.id === clicked.id);
64
+ onMarkerClick(markers[index] ?? clicked);
65
+ },
66
+ onCameraMove: ({ coordinates, zoom: nextZoom }) =>
67
+ onRegionChange({ center: coordinates, zoom: nextZoom }),
68
+ };
69
+ if (Platform.OS === 'ios') {
70
+ return (
71
+ <AppleMaps.View
72
+ cameraPosition={cameraPosition}
73
+ markers={mapMarkers}
74
+ polylines={lines}
75
+ properties={{ isMyLocationEnabled: showsUserLocation, mapType: APPLE_MAP_TYPES[mapType] }}
76
+ style={style}
77
+ {...events}
78
+ />
79
+ );
80
+ }
81
+ return (
82
+ <GoogleMaps.View
83
+ cameraPosition={cameraPosition}
84
+ markers={mapMarkers}
85
+ polylines={lines}
86
+ properties={{ isMyLocationEnabled: showsUserLocation, mapType: GOOGLE_MAP_TYPES[mapType] }}
87
+ style={style}
88
+ {...events}
89
+ />
90
+ );
91
+ }
92
+
93
+ export default MapView;
@@ -0,0 +1,86 @@
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, { useEffect, useState } from 'react';
18
+ import { Modal, Pressable, Text, View } from 'react-native';
19
+
20
+ import ScannerView from './ScannerView.js';
21
+ import scanRequests from './scanRequests.js';
22
+
23
+ // The full-screen camera the ScanCode action opens: the first code read answers it; Cancel answers
24
+ // null. Mounted once at the root (DeviceHosts).
25
+ function ScanCodeSheet() {
26
+ const [request, setRequest] = useState(scanRequests.get());
27
+ const [torch, setTorch] = useState(false);
28
+ useEffect(() => scanRequests.subscribe(setRequest), []);
29
+ useEffect(() => setTorch(request?.params.torch === true), [request]);
30
+ if (request === null) {
31
+ return null;
32
+ }
33
+ return (
34
+ <Modal
35
+ animationType="slide"
36
+ onRequestClose={() => scanRequests.finish(null)}
37
+ presentationStyle="fullScreen"
38
+ visible
39
+ >
40
+ <View style={{ backgroundColor: '#000000', flex: 1 }}>
41
+ <ScannerView
42
+ onScan={(read) => scanRequests.finish(read)}
43
+ style={{ flex: 1 }}
44
+ torch={torch}
45
+ types={request.params.types}
46
+ />
47
+ {request.params.prompt ? (
48
+ <Text
49
+ style={{
50
+ color: '#FFFFFF',
51
+ fontSize: 17,
52
+ left: 24,
53
+ position: 'absolute',
54
+ right: 24,
55
+ textAlign: 'center',
56
+ top: 80,
57
+ }}
58
+ >
59
+ {request.params.prompt}
60
+ </Text>
61
+ ) : null}
62
+ <View
63
+ style={{
64
+ bottom: 48,
65
+ flexDirection: 'row',
66
+ justifyContent: 'space-between',
67
+ left: 24,
68
+ position: 'absolute',
69
+ right: 24,
70
+ }}
71
+ >
72
+ <Pressable accessibilityRole="button" onPress={() => scanRequests.finish(null)}>
73
+ <Text style={{ color: '#FFFFFF', fontSize: 17 }}>Cancel</Text>
74
+ </Pressable>
75
+ <Pressable accessibilityRole="button" onPress={() => setTorch((on) => !on)}>
76
+ <Text style={{ color: '#FFFFFF', fontSize: 17 }}>
77
+ {torch ? 'Torch off' : 'Torch on'}
78
+ </Text>
79
+ </Pressable>
80
+ </View>
81
+ </View>
82
+ </Modal>
83
+ );
84
+ }
85
+
86
+ export default ScanCodeSheet;
@@ -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 React from 'react';
18
+ import { CameraView } from 'expo-camera';
19
+
20
+ const ALL_TYPES = [
21
+ 'qr',
22
+ 'ean13',
23
+ 'ean8',
24
+ 'code128',
25
+ 'code39',
26
+ 'code93',
27
+ 'upc_a',
28
+ 'upc_e',
29
+ 'pdf417',
30
+ 'datamatrix',
31
+ 'aztec',
32
+ 'itf14',
33
+ 'codabar',
34
+ ];
35
+
36
+ /**
37
+ * The live code scanner the QRScanner block and the ScanCode sheet draw: expo-camera's camera with
38
+ * barcode scanning (DataScanner on iOS, ML Kit on Android). Each read calls onScan({ data, type }).
39
+ */
40
+ function ScannerView({ onScan, style, torch, types }) {
41
+ return (
42
+ <CameraView
43
+ barcodeScannerSettings={{ barcodeTypes: types ?? ALL_TYPES }}
44
+ enableTorch={torch === true}
45
+ facing="back"
46
+ onBarcodeScanned={({ data, type }) => onScan({ data, type })}
47
+ style={style}
48
+ />
49
+ );
50
+ }
51
+
52
+ export default ScannerView;
@@ -0,0 +1,70 @@
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 {
18
+ AudioModule,
19
+ RecordingPresets,
20
+ requestRecordingPermissionsAsync,
21
+ setAudioModeAsync,
22
+ } from 'expo-audio';
23
+ import { UserError } from '@lowdefy/errors';
24
+
25
+ import toPickedFile from '../basic/toPickedFile.js';
26
+
27
+ /**
28
+ * The audio device feature (profile field): one voice note at a time (StartRecording,
29
+ * StopRecording). Stop returns the recording as a file with its duration, or null when nothing was
30
+ * recording.
31
+ */
32
+ function createAudioFeature() {
33
+ let recorder = null;
34
+ let startedAt = null;
35
+ return {
36
+ async start({ quality = 'high' } = {}) {
37
+ const { granted } = await requestRecordingPermissionsAsync();
38
+ if (!granted) {
39
+ throw new UserError('Microphone permission was denied.');
40
+ }
41
+ if (recorder !== null) {
42
+ await recorder.stop();
43
+ }
44
+ await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });
45
+ recorder = new AudioModule.AudioRecorder(
46
+ quality === 'low' ? RecordingPresets.LOW_QUALITY : RecordingPresets.HIGH_QUALITY
47
+ );
48
+ await recorder.prepareToRecordAsync();
49
+ recorder.record();
50
+ startedAt = Date.now();
51
+ return { recording: true };
52
+ },
53
+ async stop({ base64 = false } = {}) {
54
+ if (recorder === null) {
55
+ return null;
56
+ }
57
+ const current = recorder;
58
+ recorder = null;
59
+ await current.stop();
60
+ await setAudioModeAsync({ allowsRecording: false });
61
+ const file = await toPickedFile({
62
+ asset: { mimeType: 'audio/mp4', name: `voice-note-${startedAt}.m4a`, uri: current.uri },
63
+ base64,
64
+ });
65
+ return { ...file, duration: (Date.now() - startedAt) / 1000 };
66
+ },
67
+ };
68
+ }
69
+
70
+ export default createAudioFeature;
@@ -0,0 +1,40 @@
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 Brightness from 'expo-brightness';
18
+ import { type } from '@lowdefy/helpers';
19
+
20
+ /**
21
+ * The brightness device feature (profile field): SetBrightness sets the app's screen brightness,
22
+ * for a scan screen, and gives it back to the system with `reset`. The app's brightness ends when it
23
+ * goes to the background.
24
+ */
25
+ function createBrightnessFeature() {
26
+ return {
27
+ async set({ level, reset }) {
28
+ if (reset === true) {
29
+ await Brightness.restoreSystemBrightnessAsync();
30
+ return;
31
+ }
32
+ if (!type.isNumber(level)) {
33
+ throw new Error('SetBrightness requires "level" from 0 to 1, or "reset: true".');
34
+ }
35
+ await Brightness.setBrightnessAsync(Math.min(1, Math.max(0, level)));
36
+ },
37
+ };
38
+ }
39
+
40
+ export default createBrightnessFeature;
@@ -0,0 +1,56 @@
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 { Camera } from 'expo-camera';
18
+ import * as ImagePicker from 'expo-image-picker';
19
+ import { UserError } from '@lowdefy/errors';
20
+
21
+ import scanRequests from './scanRequests.js';
22
+ import ScannerView from './ScannerView.js';
23
+ import toImagePickerOptions from '../basic/toImagePickerOptions.js';
24
+ import toPickedFile from '../basic/toPickedFile.js';
25
+
26
+ /**
27
+ * The camera device feature (profile field): ScanCode's sheet, TakePhoto's capture and the live
28
+ * ScannerView the QRScanner block draws. Each asks for the camera the first time; a refusal throws.
29
+ */
30
+ function createCameraFeature() {
31
+ async function allowCamera() {
32
+ const { granted } = await Camera.requestCameraPermissionsAsync();
33
+ if (!granted) {
34
+ throw new UserError('Camera permission was denied.');
35
+ }
36
+ }
37
+ return {
38
+ ScannerView,
39
+ async scanCode(params) {
40
+ await allowCamera();
41
+ return scanRequests.open(params);
42
+ },
43
+ async takePhoto(params) {
44
+ await allowCamera();
45
+ const result = await ImagePicker.launchCameraAsync(toImagePickerOptions(params));
46
+ if (result.canceled) {
47
+ return [];
48
+ }
49
+ return Promise.all(
50
+ result.assets.map((asset) => toPickedFile({ asset, base64: params.base64 === true }))
51
+ );
52
+ },
53
+ };
54
+ }
55
+
56
+ export default createCameraFeature;
@@ -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
+ import DocumentScanner, { ResponseType } from 'react-native-document-scanner-plugin';
18
+
19
+ import toPickedFile from '../basic/toPickedFile.js';
20
+
21
+ /**
22
+ * The documentScanner device feature (profile field): the system document scanner (VisionKit on
23
+ * iOS, ML Kit on Android) finds each page's edges, crops and straightens it. Pages come back as
24
+ * JPEG files; [] when cancelled.
25
+ */
26
+ function createDocumentScannerFeature() {
27
+ return async function scanDocument({ base64 = false, maxPages, quality = 0.8 }) {
28
+ const { scannedImages, status } = await DocumentScanner.scanDocument({
29
+ croppedImageQuality: Math.round(quality * 100),
30
+ maxNumDocuments: maxPages,
31
+ responseType: ResponseType.ImageFilePath,
32
+ });
33
+ if (status === 'cancel' || !Array.isArray(scannedImages)) {
34
+ return [];
35
+ }
36
+ return Promise.all(
37
+ scannedImages.map((uri, index) =>
38
+ toPickedFile({
39
+ asset: {
40
+ mimeType: 'image/jpeg',
41
+ name: `scan-${Date.now()}-${index + 1}.jpg`,
42
+ uri: uri.startsWith('file://') ? uri : `file://${uri}`,
43
+ },
44
+ base64,
45
+ })
46
+ )
47
+ );
48
+ };
49
+ }
50
+
51
+ export default createDocumentScannerFeature;