@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
package/app.config.ts ADDED
@@ -0,0 +1,183 @@
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 { execFileSync } from 'node:child_process';
18
+ import crypto from 'node:crypto';
19
+ import fs from 'node:fs';
20
+ import { createRequire } from 'node:module';
21
+ import path from 'node:path';
22
+
23
+ import type { ConfigContext, ExpoConfig } from 'expo/config';
24
+
25
+ import { getAppLinkIntentFilters, getAssociatedDomains } from './appLinks.js';
26
+ import applyShellPlugins from './applyShellPlugins.js';
27
+ import { getAuthCallbackIntentFilters } from './authCallbackIntentFilters.js';
28
+ import checkShellPlugins from './checkShellPlugins.js';
29
+ import getPrivacyManifest from './getPrivacyManifest.js';
30
+ import { pluginName } from './profiles.js';
31
+ import readShellConfig from './readShellConfig.js';
32
+ import resolveShellPlugins from './resolveShellPlugins.js';
33
+ import getRuntimeVersion from './runtimeVersion.js';
34
+ import { getExclusions, getShellProfile, getShellVariant } from './shellVariant.js';
35
+ import withExcludedNativeModules from './withExcludedNativeModules.js';
36
+
37
+ // The shell's config is its shell.json (readShellConfig.js). Its capability profile is added here,
38
+ // at build, so the app reads one object (src/config/getShellConfig.js). The shell's version and
39
+ // type manifest are not: they travel in the JavaScript bundle (src/version/shellVersion.js,
40
+ // src/plugins/typeManifest.js), since an embedded launch reads the Expo config of the native build,
41
+ // not that of the JavaScript it runs.
42
+ //
43
+ // Three versions: `version` is the store version (shell.json `version`, set per build by
44
+ // `lowdefy mobile`), kept out of `extra.shell`; the shell version is the @lowdefy/shell package
45
+ // version, which the runtime sends as `x-lowdefy-shell`; `runtimeVersion` is
46
+ // `<nativeVersion>-<profile>[-<plugins hash>]` (runtimeVersion.js), which decides which
47
+ // over-the-air updates a binary takes.
48
+ const shellRoot = __dirname;
49
+
50
+ function readJson(filePath: string) {
51
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
52
+ }
53
+
54
+ // The shell's icon (shell.json `icon`, a 1024×1024 PNG, relative to shell.json) and its Android
55
+ // adaptive-icon foreground: the icon padded into the launcher's safe zone, written beside shell.json
56
+ // once per icon (scripts/createShellAssets.mjs) and reused while the icon is unchanged.
57
+ function createShellIcons({ configPath, shell }) {
58
+ const directory = path.dirname(configPath);
59
+ const icon = path.resolve(directory, shell.icon);
60
+ const iconHash = crypto
61
+ .createHash('sha256')
62
+ .update(fs.readFileSync(icon))
63
+ .digest('hex')
64
+ .slice(0, 12);
65
+ const adaptiveForeground = path.join(directory, `adaptive-icon-${iconHash}.png`);
66
+ if (!fs.existsSync(adaptiveForeground)) {
67
+ execFileSync(process.execPath, [
68
+ path.join(shellRoot, 'scripts/createShellAssets.mjs'),
69
+ icon,
70
+ adaptiveForeground,
71
+ ]);
72
+ }
73
+ return { adaptiveForeground, icon };
74
+ }
75
+
76
+ // The splash shows the shell's icon on its colour.
77
+ function withSplash({ color, icon, plugins }) {
78
+ return plugins.map((plugin) =>
79
+ pluginName(plugin) === 'expo-splash-screen'
80
+ ? ['expo-splash-screen', { image: icon, imageWidth: 200, backgroundColor: color }]
81
+ : plugin
82
+ );
83
+ }
84
+
85
+ export default function appConfig({ config }: ConfigContext): ExpoConfig {
86
+ const { configPath, shell } = readShellConfig();
87
+ const { version: storeVersion, ...shellExtra } = shell;
88
+ const variant = getShellVariant();
89
+ const { nativeVersion } = readJson(path.join(shellRoot, 'package.json'));
90
+ // The shell carries only its capability profile's native modules (profiles.js). The rest, and
91
+ // Lowdefy Dev's developer features in a release shell, are left out: not linked, their config
92
+ // plugins and permissions not applied (scripts/checkReleaseBundle.mjs checks it).
93
+ // Its shell plugins (shell.json `plugins`, installed in the shell project by `lowdefy mobile
94
+ // config`) add their own modules, config plugins, permissions, privacy reasons and data on top.
95
+ // Their modules are never excluded: the exclusions name only modules of the shell's own profiles.
96
+ const shellPlugins = resolveShellPlugins({
97
+ shell,
98
+ require: createRequire(path.join(shellRoot, 'package.json')),
99
+ });
100
+ checkShellPlugins({ shell, variant, plugins: shellPlugins });
101
+ const profile = applyShellPlugins({
102
+ profile: getShellProfile({ shell, variant }),
103
+ plugins: shellPlugins,
104
+ });
105
+ const excluded = getExclusions({ shell, variant });
106
+ // Lowdefy Dev has no icon of its own and keeps Expo's.
107
+ const icons = shell.icon === undefined ? null : createShellIcons({ configPath, shell });
108
+ const color = shell.color ?? '#FFFFFF';
109
+ const appConfigResult: ExpoConfig = {
110
+ ...config,
111
+ name: shell.name,
112
+ slug: shell.id,
113
+ // Lowdefy Dev has no store version of its own here.
114
+ version: storeVersion ?? '0.0.0',
115
+ runtimeVersion: getRuntimeVersion({
116
+ nativeVersion,
117
+ profile: profile.name,
118
+ plugins: shell.plugins,
119
+ extensions: shell.extensions,
120
+ }),
121
+ platforms: ['ios', 'android'],
122
+ scheme: shell.scheme,
123
+ orientation: 'default',
124
+ userInterfaceStyle: 'automatic',
125
+ ...(icons ? { icon: icons.icon } : {}),
126
+ ios: {
127
+ bundleIdentifier: shell.ios?.bundleIdentifier,
128
+ supportsTablet: true,
129
+ // The shell uses only the encryption the OS provides (HTTPS, the Keychain), so App Store
130
+ // Connect asks no export compliance question on upload.
131
+ config: { usesNonExemptEncryption: false },
132
+ // The profile's purpose texts no config plugin writes (profiles.js `infoPlist`).
133
+ // expo-dev-menu's floating tools button sits over the header's trailing actions in a
134
+ // development build; the dev menu stays a shake away (DevMenuPreferences.swift reads this).
135
+ infoPlist: { ...profile.infoPlist, EXDevMenuShowFloatingActionButton: false },
136
+ // Links to the app's pages open in the shell (appLinks.js).
137
+ associatedDomains: getAssociatedDomains({ shell }),
138
+ privacyManifests: getPrivacyManifest({ privacyReasons: profile.privacyReasons }),
139
+ },
140
+ android: {
141
+ package: shell.android?.package,
142
+ ...(icons
143
+ ? { adaptiveIcon: { foregroundImage: icons.adaptiveForeground, backgroundColor: color } }
144
+ : {}),
145
+ // The sign-in sheet returns by a verified app link on Android (src/auth/signInWithSheet.js).
146
+ // Links to the app's pages open in the shell too (appLinks.js).
147
+ intentFilters: [
148
+ ...getAuthCallbackIntentFilters({ shell }),
149
+ ...getAppLinkIntentFilters({ shell }),
150
+ ],
151
+ // Google Maps draws the Map block on Android. Its key is a build secret, never in
152
+ // lowdefy.yaml or the bundle.
153
+ ...(profile.capabilities.includes('maps') && process.env.LOWDEFY_GOOGLE_MAPS_API_KEY
154
+ ? { config: { googleMaps: { apiKey: process.env.LOWDEFY_GOOGLE_MAPS_API_KEY } } }
155
+ : {}),
156
+ blockedPermissions: excluded.permissions.filter((permission) =>
157
+ permission.startsWith('android.permission.')
158
+ ),
159
+ },
160
+ plugins: icons
161
+ ? withSplash({ color, icon: icons.icon, plugins: profile.plugins })
162
+ : profile.plugins,
163
+ // A shell built for the stores is its own EAS project, which serves its over-the-air updates.
164
+ // Lowdefy Dev's shell.dev.json has none.
165
+ ...(shell.eas
166
+ ? {
167
+ owner: shell.eas.owner,
168
+ updates: { url: `https://u.expo.dev/${shell.eas.projectId}` },
169
+ }
170
+ : {}),
171
+ extra: {
172
+ ...(shell.eas ? { eas: { projectId: shell.eas.projectId } } : {}),
173
+ shell: {
174
+ ...shellExtra,
175
+ profile: profile.name,
176
+ // The profile's and its extensions' capabilities, with any shell.json lists beside them.
177
+ capabilities: [...new Set([...(shell.capabilities ?? []), ...profile.capabilities])],
178
+ variant,
179
+ },
180
+ },
181
+ };
182
+ return withExcludedNativeModules(appConfigResult, Object.keys(excluded.modules));
183
+ }
package/appLinks.js ADDED
@@ -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
+ // The app's links that open in the shell: https links to any page of the app (universal links on
18
+ // iOS, verified app links on Android). The server's apple-app-site-association and assetlinks.json
19
+ // name the shell for every page path under basePath and leave /api to the server; the shell
20
+ // declares the domain (iOS) and the host and path prefix (Android) of its app's url for its
21
+ // environment. Both platforms verify https links only, so an http url (a local dev server) gets
22
+ // none.
23
+ function getAppUrl({ shell }) {
24
+ const url = shell.app.urls[shell.environment];
25
+ if (!url.startsWith('https://')) {
26
+ return null;
27
+ }
28
+ return new URL(url);
29
+ }
30
+
31
+ // iOS: the Associated Domains entitlement, `applinks:<host>` (with its port when it has one).
32
+ function getAssociatedDomains({ shell }) {
33
+ const url = getAppUrl({ shell });
34
+ if (url === null) {
35
+ return [];
36
+ }
37
+ return [`applinks:${url.host}`];
38
+ }
39
+
40
+ // Android: one autoVerify filter for the app's host under its basePath. The sign-in callback has its
41
+ // own filter (authCallbackIntentFilters.js); the router hands that path to the sign-in sheet.
42
+ function getAppLinkIntentFilters({ shell }) {
43
+ const url = getAppUrl({ shell });
44
+ if (url === null) {
45
+ return [];
46
+ }
47
+ const basePath = url.pathname.replace(/\/$/, '');
48
+ const data = { scheme: 'https', host: url.hostname };
49
+ if (basePath !== '') {
50
+ data.pathPrefix = basePath;
51
+ }
52
+ if (url.port !== '') {
53
+ data.port = url.port;
54
+ }
55
+ return [
56
+ {
57
+ action: 'VIEW',
58
+ autoVerify: true,
59
+ data: [data],
60
+ category: ['BROWSABLE', 'DEFAULT'],
61
+ },
62
+ ];
63
+ }
64
+
65
+ module.exports = { getAppLinkIntentFilters, getAssociatedDomains };
@@ -0,0 +1,43 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ const { mergeProfiles } = require('./profiles.js');
18
+
19
+ /**
20
+ * A shell's resolved profile with its plugins' `./shell` entries folded in (resolveShellPlugins.js):
21
+ * their capabilities, modules, config plugins, permissions, privacy reasons and data join the
22
+ * profile's. The profile keeps its name, and its JavaScript entries are its own.
23
+ *
24
+ * @param {Object} params
25
+ * @param {Object} params.profile The shell's resolved profile (getShellProfile in shellVariant.js).
26
+ * @param {{ entry: Object }[]} params.plugins The resolved shell plugins.
27
+ */
28
+ function applyShellPlugins({ profile, plugins }) {
29
+ return plugins.reduce(
30
+ (merged, { entry }) =>
31
+ mergeProfiles(merged, {
32
+ capabilities: entry.capabilities,
33
+ modules: entry.modules,
34
+ plugins: entry.plugins,
35
+ permissions: entry.permissions,
36
+ privacyReasons: entry.privacyReasons,
37
+ dataCollected: entry.dataCollected,
38
+ }),
39
+ profile
40
+ );
41
+ }
42
+
43
+ module.exports = applyShellPlugins;
@@ -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
+ // The Android app link the sign-in sheet returns by: the app server sends the one-time token to
18
+ // https://<app domain><basePath>/auth/mobile/callback, and Android opens that link only in the
19
+ // shell the app's assetlinks.json names (autoVerify), never in another app. A shell opens one app
20
+ // (checkShellConfig), so there is one filter, at the app's url for the shell's environment.
21
+ // Android verifies https links only, so an http url (a local dev server) gets none.
22
+ function getAuthCallbackIntentFilters({ shell }) {
23
+ const url = shell.app.urls[shell.environment];
24
+ if (!url.startsWith('https://')) {
25
+ return [];
26
+ }
27
+ const { hostname, pathname, port } = new URL(url);
28
+ const data = {
29
+ scheme: 'https',
30
+ host: hostname,
31
+ pathPrefix: `${pathname.replace(/\/$/, '')}/auth/mobile/callback`,
32
+ };
33
+ if (port !== '') {
34
+ data.port = port;
35
+ }
36
+ return [
37
+ {
38
+ action: 'VIEW',
39
+ autoVerify: true,
40
+ data: [data],
41
+ category: ['BROWSABLE', 'DEFAULT'],
42
+ },
43
+ ];
44
+ }
45
+
46
+ module.exports = { getAuthCallbackIntentFilters };
@@ -0,0 +1,25 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // intl-messageformat (through @lowdefy/helpers translate) ships static class blocks, which
18
+ // babel-preset-expo leaves alone; Metro and Jest transform node_modules with this config too.
19
+ module.exports = function babelConfig(api) {
20
+ api.cache(true);
21
+ return {
22
+ presets: ['babel-preset-expo'],
23
+ plugins: ['@babel/plugin-transform-class-static-block'],
24
+ };
25
+ };
@@ -0,0 +1,172 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ const { EXTENSIONS } = require('./profiles.js');
18
+
19
+ function isObject(value) {
20
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
21
+ }
22
+
23
+ // A shell opens exactly one Lowdefy app: every app's `_js` runs in the shell's one JavaScript realm,
24
+ // so two apps in one shell would share its session tokens, storage and cache. app.config.ts (and
25
+ // so `expo prebuild` and `expo export`) refuses a shell.json that lists `apps`, or that does not
26
+ // name one app with a url for the shell's environment.
27
+ function checkShellConfig({ configPath, shell }) {
28
+ const where = `Shell config "${configPath}"`;
29
+ if (!isObject(shell)) {
30
+ throw new Error(`${where} should be an object.`);
31
+ }
32
+ if (Object.prototype.hasOwnProperty.call(shell, 'apps')) {
33
+ throw new Error(
34
+ `${where} lists "apps". A shell opens exactly one app: replace "apps" with "app": { id, name, urls: { <environment>: url } }.`
35
+ );
36
+ }
37
+ if (Array.isArray(shell.app)) {
38
+ throw new Error(
39
+ `${where} names ${shell.app.length} apps in "app". A shell opens exactly one app: "app" should be one { id, name, urls } object.`
40
+ );
41
+ }
42
+ if (!isObject(shell.app)) {
43
+ throw new Error(
44
+ `${where} should name the app it opens as "app": { id, name, urls: { <environment>: url } }. Received ${JSON.stringify(
45
+ shell.app
46
+ )}.`
47
+ );
48
+ }
49
+ if (typeof shell.app.id !== 'string' || shell.app.id.length === 0) {
50
+ throw new Error(
51
+ `${where} "app.id" should be a string. Received ${JSON.stringify(shell.app.id)}.`
52
+ );
53
+ }
54
+ const url = isObject(shell.app.urls) ? shell.app.urls[shell.environment] : undefined;
55
+ if (typeof url !== 'string' || url.length === 0) {
56
+ throw new Error(
57
+ `${where} "app.urls" should have a url for the shell's environment "${
58
+ shell.environment
59
+ }". Received ${JSON.stringify(shell.app.urls)}.`
60
+ );
61
+ }
62
+ // The shell reads the server's origin from this url (src/host/getOrigin.js).
63
+ if (!/^[a-z][a-z0-9+.-]*:\/\/[^/?#]+/i.test(url)) {
64
+ throw new Error(
65
+ `${where} "app.urls.${
66
+ shell.environment
67
+ }" should be an absolute URL, like "https://app.example.com". Received ${JSON.stringify(
68
+ url
69
+ )}.`
70
+ );
71
+ }
72
+ if (shell.profile !== undefined && typeof shell.profile !== 'string') {
73
+ throw new Error(
74
+ `${where} "profile" should be the name of a capability profile. Received ${JSON.stringify(
75
+ shell.profile
76
+ )}.`
77
+ );
78
+ }
79
+ // The store version, major.minor.patch; `lowdefy mobile build` sets the patch.
80
+ if (shell.version !== undefined && typeof shell.version !== 'string') {
81
+ throw new Error(
82
+ `${where} "version" should be a string. Received ${JSON.stringify(shell.version)}.`
83
+ );
84
+ }
85
+ // The icon is a path relative to shell.json; `lowdefy mobile config` copies it there.
86
+ if (shell.icon !== undefined && (typeof shell.icon !== 'string' || shell.icon.length === 0)) {
87
+ throw new Error(
88
+ `${where} "icon" should be the path of a PNG, relative to the shell config. Received ${JSON.stringify(
89
+ shell.icon
90
+ )}.`
91
+ );
92
+ }
93
+ if (shell.color !== undefined && !/^#[0-9a-fA-F]{6}$/.test(shell.color)) {
94
+ throw new Error(
95
+ `${where} "color" should be a colour like "#0B5FFF". Received ${JSON.stringify(shell.color)}.`
96
+ );
97
+ }
98
+ if (shell.eas !== undefined) {
99
+ if (!isObject(shell.eas) || typeof shell.eas.projectId !== 'string') {
100
+ throw new Error(
101
+ `${where} "eas" should be { owner, projectId }, the shell's Expo account and project. Received ${JSON.stringify(
102
+ shell.eas
103
+ )}.`
104
+ );
105
+ }
106
+ if (shell.eas.owner !== undefined && typeof shell.eas.owner !== 'string') {
107
+ throw new Error(
108
+ `${where} "eas.owner" should be a string. Received ${JSON.stringify(shell.eas.owner)}.`
109
+ );
110
+ }
111
+ }
112
+ // The opt-in extensions (EXTENSIONS in profiles.js), each named once.
113
+ if (shell.extensions !== undefined) {
114
+ const known = Object.keys(EXTENSIONS);
115
+ if (
116
+ !Array.isArray(shell.extensions) ||
117
+ !shell.extensions.every((name) => known.includes(name)) ||
118
+ new Set(shell.extensions).size !== shell.extensions.length
119
+ ) {
120
+ throw new Error(
121
+ `${where} "extensions" should list each of ${known.join(
122
+ ', '
123
+ )} at most once. Received ${JSON.stringify(shell.extensions)}.`
124
+ );
125
+ }
126
+ }
127
+ // The shell plugins `lowdefy mobile config` installs into the shell project
128
+ // (resolveShellPlugins.js).
129
+ if (shell.plugins !== undefined) {
130
+ checkPlugins({ plugins: shell.plugins, where });
131
+ }
132
+ return shell;
133
+ }
134
+
135
+ function checkPlugins({ plugins, where }) {
136
+ if (!Array.isArray(plugins)) {
137
+ throw new Error(
138
+ `${where} "plugins" should be an array of { name, version, typePrefix?, options? }. Received ${JSON.stringify(
139
+ plugins
140
+ )}.`
141
+ );
142
+ }
143
+ plugins.forEach((plugin, index) => {
144
+ const at = `${where} "plugins[${index}]"`;
145
+ if (!isObject(plugin)) {
146
+ throw new Error(
147
+ `${at} should be { name, version, typePrefix?, options? }. Received ${JSON.stringify(
148
+ plugin
149
+ )}.`
150
+ );
151
+ }
152
+ ['name', 'version'].forEach((key) => {
153
+ if (typeof plugin[key] !== 'string' || plugin[key].length === 0) {
154
+ throw new Error(
155
+ `${at} "${key}" should be a string. Received ${JSON.stringify(plugin[key])}.`
156
+ );
157
+ }
158
+ });
159
+ if (plugin.typePrefix !== undefined && typeof plugin.typePrefix !== 'string') {
160
+ throw new Error(
161
+ `${at} "typePrefix" should be a string. Received ${JSON.stringify(plugin.typePrefix)}.`
162
+ );
163
+ }
164
+ if (plugin.options !== undefined && !isObject(plugin.options)) {
165
+ throw new Error(
166
+ `${at} "options" should be an object. Received ${JSON.stringify(plugin.options)}.`
167
+ );
168
+ }
169
+ });
170
+ }
171
+
172
+ module.exports = checkShellConfig;
@@ -0,0 +1,74 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ const { PROFILES, RELEASE_PROFILES, resolveProfile } = require('./profiles.js');
18
+ const { getProfileName, getShellProfile } = require('./shellVariant.js');
19
+
20
+ // Release profiles first, lowest first, so an error names the smallest profile a shell can pick.
21
+ const PROFILE_ORDER = [
22
+ ...RELEASE_PROFILES,
23
+ ...Object.keys(PROFILES).filter((name) => !RELEASE_PROFILES.includes(name)),
24
+ ];
25
+
26
+ function findProfile(carries) {
27
+ return PROFILE_ORDER.find((name) => carries(resolveProfile(name)));
28
+ }
29
+
30
+ /**
31
+ * Refuses shell plugins that would carry native code around the capability profiles, so plugins
32
+ * never become a way past profile review. Throws, naming the plugin and the profile to pick, when
33
+ * a plugin's `./shell` entry:
34
+ * - links a module @lowdefy/shell depends on itself (one of any profile's, such as expo-camera); or
35
+ * - asks for a permission some profile carries and this shell's profile leaves out (the camera on
36
+ * `basic`).
37
+ *
38
+ * `lowdefy mobile config` runs it from the copied shell, and app.config.ts runs it again, so a
39
+ * hand-edited shell.json is refused at prebuild.
40
+ *
41
+ * @param {Object} params
42
+ * @param {Object} params.shell The shell config (shell.json).
43
+ * @param {string} params.variant The shell variant, dev or release.
44
+ * @param {{ name: string, entry: Object }[]} params.plugins The resolved shell plugins
45
+ * (resolveShellPlugins.js).
46
+ */
47
+ function checkShellPlugins({ shell, variant, plugins }) {
48
+ const profileName = getProfileName({ shell, variant });
49
+ const profile = getShellProfile({ shell, variant });
50
+ plugins.forEach(({ name, entry }) => {
51
+ Object.keys(entry.modules ?? {}).forEach((module) => {
52
+ const carrier = findProfile((candidate) => module in candidate.modules);
53
+ if (carrier !== undefined) {
54
+ throw new Error(
55
+ `Shell plugin "${name}" links "${module}", which @lowdefy/shell carries in the "${carrier}" profile. Pick the profile that carries it instead of adding it with a plugin.`
56
+ );
57
+ }
58
+ });
59
+ (entry.permissions ?? []).forEach((permission) => {
60
+ if (profile.permissions.includes(permission)) {
61
+ return;
62
+ }
63
+ const carrier = findProfile((candidate) => candidate.permissions.includes(permission));
64
+ if (carrier !== undefined) {
65
+ throw new Error(
66
+ `Shell plugin "${name}" asks for "${permission}", which the "${profileName}" profile leaves out. Pick the "${carrier}" profile, which carries it.`
67
+ );
68
+ }
69
+ });
70
+ });
71
+ return plugins;
72
+ }
73
+
74
+ module.exports = checkShellPlugins;
@@ -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
+ /**
18
+ * The iOS privacy manifest (`ios.privacyManifests`) of a shell on a capability profile: the
19
+ * required-reason APIs its modules use (profiles.js `privacyReasons`, where `basic` holds React
20
+ * Native's and expo-sqlite's, so every profile carries them), each category listed once. App Store
21
+ * Connect refuses an upload whose binary uses one of these APIs without a declared reason.
22
+ *
23
+ * @param {Object} params
24
+ * @param {{ category: string, reasons: string[] }[]} params.privacyReasons The resolved profile's.
25
+ */
26
+ function getPrivacyManifest({ privacyReasons }) {
27
+ const byCategory = new Map();
28
+ privacyReasons.forEach(({ category, reasons }) => {
29
+ byCategory.set(category, [...new Set([...(byCategory.get(category) ?? []), ...reasons])]);
30
+ });
31
+ return {
32
+ NSPrivacyAccessedAPITypes: [...byCategory.entries()].map(([category, reasons]) => ({
33
+ NSPrivacyAccessedAPIType: category,
34
+ NSPrivacyAccessedAPITypeReasons: reasons,
35
+ })),
36
+ };
37
+ }
38
+
39
+ module.exports = getPrivacyManifest;