@lowdefy/shell 0.0.0-experimental-20261019052246

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 (239) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +308 -0
  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 +30 -0
  7. package/app/_layout.js +33 -0
  8. package/app.config.ts +168 -0
  9. package/applyShellPlugins.js +43 -0
  10. package/authCallbackIntentFilters.js +46 -0
  11. package/babel.config.js +25 -0
  12. package/checkShellConfig.js +145 -0
  13. package/checkShellPlugins.js +74 -0
  14. package/getPrivacyManifest.js +39 -0
  15. package/metro.config.js +73 -0
  16. package/native-fingerprint.json +12 -0
  17. package/package.json +130 -0
  18. package/profiles.js +274 -0
  19. package/readShellConfig.js +36 -0
  20. package/resolveShellPlugins.js +109 -0
  21. package/runtimeVersion.js +105 -0
  22. package/scripts/checkReleaseBundle.mjs +358 -0
  23. package/scripts/createDayjsLocales.mjs +67 -0
  24. package/scripts/createShellAssets.mjs +54 -0
  25. package/scripts/createShellVersion.mjs +83 -0
  26. package/scripts/createTypeManifest.mjs +205 -0
  27. package/shell.dev.json +11 -0
  28. package/shellVariant.js +111 -0
  29. package/src/ShellContext.js +32 -0
  30. package/src/actions/CopyToClipboard.js +29 -0
  31. package/src/actions/CopyToClipboard.test.js +33 -0
  32. package/src/actions/GeolocationCurrentPosition.js +55 -0
  33. package/src/actions/GeolocationCurrentPosition.test.js +66 -0
  34. package/src/actions/GetLocalStorage.js +36 -0
  35. package/src/actions/RemoveLocalStorage.js +31 -0
  36. package/src/actions/ScrollTo.js +66 -0
  37. package/src/actions/ScrollTo.test.js +83 -0
  38. package/src/actions/SetFocus.js +33 -0
  39. package/src/actions/SetFocus.test.js +49 -0
  40. package/src/actions/SetLocalStorage.js +34 -0
  41. package/src/actions/localStorage.test.js +68 -0
  42. package/src/actions/measureInScrollView.js +36 -0
  43. package/src/actions/nativeActions.js +41 -0
  44. package/src/auth/AuthButton.js +44 -0
  45. package/src/auth/AuthGate.js +57 -0
  46. package/src/auth/AuthLink.js +43 -0
  47. package/src/auth/LockScreen.js +67 -0
  48. package/src/auth/LockScreen.test.js +37 -0
  49. package/src/auth/SignInScreen.js +256 -0
  50. package/src/auth/SignInScreen.test.js +182 -0
  51. package/src/auth/createAppAuth.js +306 -0
  52. package/src/auth/createAppAuth.test.js +414 -0
  53. package/src/auth/createAuthApi.js +99 -0
  54. package/src/auth/createAuthApi.test.js +135 -0
  55. package/src/auth/createCoreAuth.js +96 -0
  56. package/src/auth/createCoreAuth.test.js +188 -0
  57. package/src/auth/createSessionStores.js +45 -0
  58. package/src/auth/createTokenStore.js +103 -0
  59. package/src/auth/createTokenStore.test.js +116 -0
  60. package/src/auth/getAuthSettings.js +24 -0
  61. package/src/auth/getAuthView.js +39 -0
  62. package/src/auth/getAuthView.test.js +52 -0
  63. package/src/auth/getSheetReturnUrl.js +35 -0
  64. package/src/auth/getTokenStoreOptions.js +46 -0
  65. package/src/auth/isBackgroundShell.js +26 -0
  66. package/src/auth/isSheetReturnLink.js +35 -0
  67. package/src/auth/parseQuery.js +39 -0
  68. package/src/auth/purgeAppData.js +45 -0
  69. package/src/auth/shouldLock.js +26 -0
  70. package/src/auth/signInWithSheet.js +97 -0
  71. package/src/auth/signInWithSheet.test.js +205 -0
  72. package/src/auth/styles.js +78 -0
  73. package/src/auth/unregisterPushToken.js +21 -0
  74. package/src/auth/usesBiometricUnlock.js +23 -0
  75. package/src/auth/watchSession.js +47 -0
  76. package/src/auth/withoutCookies.js +26 -0
  77. package/src/cache/createSqliteCacheStore.js +113 -0
  78. package/src/cache/createSqliteCacheStore.test.js +93 -0
  79. package/src/cache/getCacheKey.js +25 -0
  80. package/src/cache/getLastCacheKeyName.js +22 -0
  81. package/src/components/AppProvider.js +90 -0
  82. package/src/components/Frame.js +116 -0
  83. package/src/components/Frame.test.js +101 -0
  84. package/src/components/LoadingScreen.js +35 -0
  85. package/src/components/MenuButton.js +108 -0
  86. package/src/components/MenuButton.test.js +51 -0
  87. package/src/components/MessageScreen.js +51 -0
  88. package/src/components/NavigationTheme.js +33 -0
  89. package/src/components/NoConnectionBanner.js +51 -0
  90. package/src/components/NoConnectionBanner.test.js +73 -0
  91. package/src/components/OtherPageScreen.js +56 -0
  92. package/src/components/PageScreen.js +244 -0
  93. package/src/components/PageScreen.test.js +492 -0
  94. package/src/components/ShellRoot.js +34 -0
  95. package/src/components/ShellRoot.test.js +62 -0
  96. package/src/components/ShellThemeProvider.js +45 -0
  97. package/src/components/SwitchToTab.js +36 -0
  98. package/src/components/SwitchToTab.test.js +63 -0
  99. package/src/components/TabIconImages.js +72 -0
  100. package/src/components/UpdateScreen.js +36 -0
  101. package/src/components/getNavigationTheme.js +44 -0
  102. package/src/components/getNavigationTheme.test.js +41 -0
  103. package/src/components/styles.js +50 -0
  104. package/src/components/useSurfaceTokens.js +47 -0
  105. package/src/config/getAppApiBase.js +31 -0
  106. package/src/config/getShellConfig.js +35 -0
  107. package/src/config/getShellConfig.test.js +80 -0
  108. package/src/createShellApp.js +105 -0
  109. package/src/dev/DevShell.js +228 -0
  110. package/src/dev/DevShell.test.js +233 -0
  111. package/src/dev/connect/ConfirmSheet.js +71 -0
  112. package/src/dev/connect/ConnectScreen.js +117 -0
  113. package/src/dev/connect/QrScanner.js +89 -0
  114. package/src/dev/connection/createDevShellConfig.js +47 -0
  115. package/src/dev/connection/createDevTransport.js +51 -0
  116. package/src/dev/connection/createDevTransport.test.js +87 -0
  117. package/src/dev/connection/createServerList.js +48 -0
  118. package/src/dev/connection/isLoopbackHost.js +25 -0
  119. package/src/dev/connection/pairedShellApp.test.js +104 -0
  120. package/src/dev/connection/parseDevLink.js +66 -0
  121. package/src/dev/connection/parseDevLink.test.js +97 -0
  122. package/src/dev/connection/parseQuery.js +33 -0
  123. package/src/dev/connection/parseServerInput.js +40 -0
  124. package/src/dev/connection/parseServerUrl.js +45 -0
  125. package/src/dev/devStyles.js +97 -0
  126. package/src/dev/index.js +21 -0
  127. package/src/dev/inject/createDeviceInjections.js +185 -0
  128. package/src/dev/inject/createDeviceInjections.test.js +208 -0
  129. package/src/dev/inject/createOfflineFetch.js +35 -0
  130. package/src/dev/inject/createOfflineFetch.test.js +68 -0
  131. package/src/dev/menu/BlockGallery.js +33 -0
  132. package/src/dev/menu/DevMenu.js +121 -0
  133. package/src/dev/menu/createLogBuffer.js +83 -0
  134. package/src/dev/menu/watchShake.js +52 -0
  135. package/src/dev/menu/watchShake.test.js +61 -0
  136. package/src/dev/recorder/createNativeRecorder.js +204 -0
  137. package/src/dev/recorder/createNativeRecorder.test.js +307 -0
  138. package/src/dev/redirectDevLink.js +30 -0
  139. package/src/dev/redirectDevLink.test.js +49 -0
  140. package/src/dev/steps/createStepRunner.js +184 -0
  141. package/src/dev/steps/createStepRunner.test.js +262 -0
  142. package/src/dev/steps/findBlock.js +78 -0
  143. package/src/dev/steps/isContextSettled.js +37 -0
  144. package/src/dev/steps/waitForSettle.js +39 -0
  145. package/src/dev/steps/waitUntil.js +40 -0
  146. package/src/dev/tab/DevTabClient.js +97 -0
  147. package/src/dev/tab/answerEval.js +39 -0
  148. package/src/dev/tab/answerInspect.js +45 -0
  149. package/src/dev/tab/answerLoadState.js +50 -0
  150. package/src/dev/tab/createDevTabClient.js +277 -0
  151. package/src/dev/tab/createDevTabClient.test.js +314 -0
  152. package/src/dev/tab/devClientMarker.js +22 -0
  153. package/src/dev/tab/devClientMarker.test.js +23 -0
  154. package/src/dev/tab/findPageContext.js +43 -0
  155. package/src/dev/tab/getShownPage.js +42 -0
  156. package/src/dev/tab/readEventStream.js +68 -0
  157. package/src/dev/tab/reloadApp.js +42 -0
  158. package/src/dev/tab/reloadApp.test.js +115 -0
  159. package/src/device/injected.js +60 -0
  160. package/src/device/injected.test.js +39 -0
  161. package/src/host/createDeviceStore.js +32 -0
  162. package/src/host/createMemoryStore.js +32 -0
  163. package/src/host/createNetworkStatus.js +94 -0
  164. package/src/host/createNetworkStatus.test.js +94 -0
  165. package/src/host/createRouterAdapter.js +33 -0
  166. package/src/host/createShellFetch.js +60 -0
  167. package/src/host/createShellFetch.test.js +150 -0
  168. package/src/host/createShellGlobals.js +62 -0
  169. package/src/host/createShellHost.js +73 -0
  170. package/src/host/createShellHost.test.js +36 -0
  171. package/src/host/getOrigin.js +26 -0
  172. package/src/host/isOnline.js +22 -0
  173. package/src/host/networkStatus.js +24 -0
  174. package/src/host/withShellHeader.js +25 -0
  175. package/src/host/withShellHeader.test.js +28 -0
  176. package/src/js/evaluateJsEntries.js +43 -0
  177. package/src/js/evaluateJsEntries.test.js +48 -0
  178. package/src/locale/createShellLocale.js +112 -0
  179. package/src/locale/createShellLocale.test.js +74 -0
  180. package/src/locale/dayjsLocales.js +165 -0
  181. package/src/locale/getDeviceLocales.js +23 -0
  182. package/src/locale/loadDayjsLocale.js +32 -0
  183. package/src/locale/resolveActiveLocale.js +46 -0
  184. package/src/locale/resolveActiveLocale.test.js +40 -0
  185. package/src/navigation/SlotContext.js +24 -0
  186. package/src/navigation/frameSlots.js +27 -0
  187. package/src/navigation/getFrame.js +30 -0
  188. package/src/navigation/getInitialScreenOptions.js +63 -0
  189. package/src/navigation/getInitialScreenOptions.test.js +66 -0
  190. package/src/navigation/getPresentation.js +28 -0
  191. package/src/navigation/getSlotRootPath.js +27 -0
  192. package/src/navigation/getStackOptions.js +185 -0
  193. package/src/navigation/getTabHref.js +45 -0
  194. package/src/navigation/getTabIcons.js +43 -0
  195. package/src/navigation/isPageUnderHeader.js +46 -0
  196. package/src/navigation/navigation.test.js +342 -0
  197. package/src/navigation/toRouterHref.js +29 -0
  198. package/src/plugins/appTypes.js +28 -0
  199. package/src/plugins/prefixTypes.js +28 -0
  200. package/src/plugins/shellPlugins.json +14 -0
  201. package/src/plugins/typeManifest.js +649 -0
  202. package/src/plugins/types.js +61 -0
  203. package/src/plugins/types.test.js +173 -0
  204. package/src/polyfills/installGetRandomValues.js +28 -0
  205. package/src/session/createAppSession.js +318 -0
  206. package/src/session/createAppSession.test.js +359 -0
  207. package/src/session/getPageView.js +59 -0
  208. package/src/session/getPageView.test.js +137 -0
  209. package/src/sync/encodePagePath.js +27 -0
  210. package/src/sync/fetchPage.js +49 -0
  211. package/src/sync/fetchRoot.js +38 -0
  212. package/src/sync/fetchUser.js +28 -0
  213. package/src/sync/runLimited.js +36 -0
  214. package/src/sync/toRootConfig.js +35 -0
  215. package/src/test/createFakeNetwork.js +40 -0
  216. package/src/test/createFakeSecureStore.js +40 -0
  217. package/src/test/createFakeServer.js +98 -0
  218. package/src/test/createMemoryCacheStore.js +59 -0
  219. package/src/test/expoImage.js +38 -0
  220. package/src/test/expoSpeechRecognition.js +31 -0
  221. package/src/test/expoUi.js +55 -0
  222. package/src/test/expoUiBottomSheet.js +64 -0
  223. package/src/test/expoUiDateTimePicker.js +26 -0
  224. package/src/test/expoUiMenu.js +46 -0
  225. package/src/test/expoUiSegmentedControl.js +47 -0
  226. package/src/test/expoUiSwiftUi.js +62 -0
  227. package/src/test/expoUiSwiftUiModifiers.js +23 -0
  228. package/src/test/expoWebBrowser.js +23 -0
  229. package/src/test/fixtures.js +98 -0
  230. package/src/test/renderPageScreen.js +109 -0
  231. package/src/types/checkTypeSet.js +39 -0
  232. package/src/types/collectPageTypes.js +49 -0
  233. package/src/types/collectPageTypes.test.js +90 -0
  234. package/src/types/findMissingPageTypes.js +40 -0
  235. package/src/version/compareVersions.js +38 -0
  236. package/src/version/isShellTooOld.js +30 -0
  237. package/src/version/shellVersion.js +21 -0
  238. package/src/version/shellVersion.test.js +32 -0
  239. package/withExcludedNativeModules.js +93 -0
@@ -0,0 +1,30 @@
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
+ // Links that are not page paths:
18
+ // - The sign-in sheet's Android app link (/auth/mobile/callback) carries a one-time token that
19
+ // signInWithSheet reads from the sheet's result; the router leaves it alone.
20
+ // - A Lowdefy Dev link (lowdefy-dev://connect, lowdefy-dev://menu): Lowdefy Dev handles it
21
+ // (src/dev). A release shell has no developer features and routes every other link.
22
+ import { redirectDevLink } from '../src/dev/index.js';
23
+ import isSheetReturnLink from '../src/auth/isSheetReturnLink.js';
24
+
25
+ export function redirectSystemPath({ initial, path }) {
26
+ if (isSheetReturnLink(path)) {
27
+ return initial ? '/' : null;
28
+ }
29
+ return redirectDevLink ? redirectDevLink({ initial, path }) : path;
30
+ }
package/app/_layout.js ADDED
@@ -0,0 +1,33 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import * as SplashScreen from 'expo-splash-screen';
19
+
20
+ import installGetRandomValues from '../src/polyfills/installGetRandomValues.js';
21
+ import ShellRoot from '../src/components/ShellRoot.js';
22
+ // Lowdefy Dev's developer features; undefined in a release shell, whose bundle leaves them out.
23
+ import { DevShell } from '../src/dev/index.js';
24
+
25
+ installGetRandomValues();
26
+
27
+ // The launch screen stays until the first screen is ready: the app's home page from the cache (or
28
+ // from the server on a first launch).
29
+ SplashScreen.preventAutoHideAsync();
30
+
31
+ export default function RootLayout() {
32
+ return DevShell ? <DevShell /> : <ShellRoot />;
33
+ }
package/app.config.ts ADDED
@@ -0,0 +1,168 @@
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 applyShellPlugins from './applyShellPlugins.js';
26
+ import { getAuthCallbackIntentFilters } from './authCallbackIntentFilters.js';
27
+ import checkShellPlugins from './checkShellPlugins.js';
28
+ import getPrivacyManifest from './getPrivacyManifest.js';
29
+ import { pluginName } from './profiles.js';
30
+ import readShellConfig from './readShellConfig.js';
31
+ import resolveShellPlugins from './resolveShellPlugins.js';
32
+ import getRuntimeVersion from './runtimeVersion.js';
33
+ import { getExclusions, getShellProfile, getShellVariant } from './shellVariant.js';
34
+ import withExcludedNativeModules from './withExcludedNativeModules.js';
35
+
36
+ // The shell's config is its shell.json (readShellConfig.js). Its capability profile is added here,
37
+ // at build, so the app reads one object (src/config/getShellConfig.js). The shell's version and
38
+ // type manifest are not: they travel in the JavaScript bundle (src/version/shellVersion.js,
39
+ // src/plugins/typeManifest.js), since an embedded launch reads the Expo config of the native build,
40
+ // not that of the JavaScript it runs.
41
+ //
42
+ // Three versions: `version` is the store version (shell.json `version`, set per build by
43
+ // `lowdefy mobile`), kept out of `extra.shell`; the shell version is the @lowdefy/shell package
44
+ // version, which the runtime sends as `x-lowdefy-shell`; `runtimeVersion` is
45
+ // `<nativeVersion>-<profile>[-<plugins hash>]` (runtimeVersion.js), which decides which
46
+ // over-the-air updates a binary takes.
47
+ const shellRoot = __dirname;
48
+
49
+ function readJson(filePath: string) {
50
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
51
+ }
52
+
53
+ // The shell's icon (shell.json `icon`, a 1024×1024 PNG, relative to shell.json) and its Android
54
+ // adaptive-icon foreground: the icon padded into the launcher's safe zone, written beside shell.json
55
+ // once per icon (scripts/createShellAssets.mjs) and reused while the icon is unchanged.
56
+ function createShellIcons({ configPath, shell }) {
57
+ const directory = path.dirname(configPath);
58
+ const icon = path.resolve(directory, shell.icon);
59
+ const iconHash = crypto
60
+ .createHash('sha256')
61
+ .update(fs.readFileSync(icon))
62
+ .digest('hex')
63
+ .slice(0, 12);
64
+ const adaptiveForeground = path.join(directory, `adaptive-icon-${iconHash}.png`);
65
+ if (!fs.existsSync(adaptiveForeground)) {
66
+ execFileSync(process.execPath, [
67
+ path.join(shellRoot, 'scripts/createShellAssets.mjs'),
68
+ icon,
69
+ adaptiveForeground,
70
+ ]);
71
+ }
72
+ return { adaptiveForeground, icon };
73
+ }
74
+
75
+ // The splash shows the shell's icon on its colour.
76
+ function withSplash({ color, icon, plugins }) {
77
+ return plugins.map((plugin) =>
78
+ pluginName(plugin) === 'expo-splash-screen'
79
+ ? ['expo-splash-screen', { image: icon, imageWidth: 200, backgroundColor: color }]
80
+ : plugin
81
+ );
82
+ }
83
+
84
+ export default function appConfig({ config }: ConfigContext): ExpoConfig {
85
+ const { configPath, shell } = readShellConfig();
86
+ const { version: storeVersion, ...shellExtra } = shell;
87
+ const variant = getShellVariant();
88
+ const { nativeVersion } = readJson(path.join(shellRoot, 'package.json'));
89
+ // The shell carries only its capability profile's native modules (profiles.js). The rest, and
90
+ // Lowdefy Dev's developer features in a release shell, are left out: not linked, their config
91
+ // plugins and permissions not applied (scripts/checkReleaseBundle.mjs checks it).
92
+ // Its shell plugins (shell.json `plugins`, installed in the shell project by `lowdefy mobile
93
+ // config`) add their own modules, config plugins, permissions, privacy reasons and data on top.
94
+ // Their modules are never excluded: the exclusions name only modules of the shell's own profiles.
95
+ const shellPlugins = resolveShellPlugins({
96
+ shell,
97
+ require: createRequire(path.join(shellRoot, 'package.json')),
98
+ });
99
+ checkShellPlugins({ shell, variant, plugins: shellPlugins });
100
+ const profile = applyShellPlugins({
101
+ profile: getShellProfile({ shell, variant }),
102
+ plugins: shellPlugins,
103
+ });
104
+ const excluded = getExclusions({ shell, variant });
105
+ // Lowdefy Dev has no icon of its own and keeps Expo's.
106
+ const icons = shell.icon === undefined ? null : createShellIcons({ configPath, shell });
107
+ const color = shell.color ?? '#FFFFFF';
108
+ const appConfigResult: ExpoConfig = {
109
+ ...config,
110
+ name: shell.name,
111
+ slug: shell.id,
112
+ // Lowdefy Dev has no store version of its own here.
113
+ version: storeVersion ?? '0.0.0',
114
+ runtimeVersion: getRuntimeVersion({
115
+ nativeVersion,
116
+ profile: profile.name,
117
+ plugins: shell.plugins,
118
+ }),
119
+ platforms: ['ios', 'android'],
120
+ scheme: shell.scheme,
121
+ orientation: 'default',
122
+ userInterfaceStyle: 'automatic',
123
+ ...(icons ? { icon: icons.icon } : {}),
124
+ ios: {
125
+ bundleIdentifier: shell.ios?.bundleIdentifier,
126
+ supportsTablet: true,
127
+ // The shell uses only the encryption the OS provides (HTTPS, the Keychain), so App Store
128
+ // Connect asks no export compliance question on upload.
129
+ config: { usesNonExemptEncryption: false },
130
+ // expo-dev-menu's floating tools button sits over the header's trailing actions in a
131
+ // development build; the dev menu stays a shake away (DevMenuPreferences.swift reads this).
132
+ infoPlist: { EXDevMenuShowFloatingActionButton: false },
133
+ privacyManifests: getPrivacyManifest({ privacyReasons: profile.privacyReasons }),
134
+ },
135
+ android: {
136
+ package: shell.android?.package,
137
+ ...(icons
138
+ ? { adaptiveIcon: { foregroundImage: icons.adaptiveForeground, backgroundColor: color } }
139
+ : {}),
140
+ // The sign-in sheet returns by a verified app link on Android (src/auth/signInWithSheet.js).
141
+ intentFilters: getAuthCallbackIntentFilters({ shell }),
142
+ blockedPermissions: excluded.permissions.filter((permission) =>
143
+ permission.startsWith('android.permission.')
144
+ ),
145
+ },
146
+ plugins: icons
147
+ ? withSplash({ color, icon: icons.icon, plugins: profile.plugins })
148
+ : profile.plugins,
149
+ // A shell built for the stores is its own EAS project, which serves its over-the-air updates.
150
+ // Lowdefy Dev's shell.dev.json has none.
151
+ ...(shell.eas
152
+ ? {
153
+ owner: shell.eas.owner,
154
+ updates: { url: `https://u.expo.dev/${shell.eas.projectId}` },
155
+ }
156
+ : {}),
157
+ extra: {
158
+ ...(shell.eas ? { eas: { projectId: shell.eas.projectId } } : {}),
159
+ shell: {
160
+ ...shellExtra,
161
+ profile: profile.name,
162
+ capabilities: shell.capabilities ?? profile.capabilities,
163
+ variant,
164
+ },
165
+ },
166
+ };
167
+ return withExcludedNativeModules(appConfigResult, Object.keys(excluded.modules));
168
+ }
@@ -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,145 @@
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
+ function isObject(value) {
18
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
19
+ }
20
+
21
+ // A shell opens exactly one Lowdefy app: every app's `_js` runs in the shell's one JavaScript realm,
22
+ // so two apps in one shell would share its session tokens, storage and cache. app.config.ts (and
23
+ // so `expo prebuild` and `expo export`) refuses a shell.json that lists `apps`, or that does not
24
+ // name one app with a url for the shell's environment.
25
+ function checkShellConfig({ configPath, shell }) {
26
+ const where = `Shell config "${configPath}"`;
27
+ if (!isObject(shell)) {
28
+ throw new Error(`${where} should be an object.`);
29
+ }
30
+ if (Object.prototype.hasOwnProperty.call(shell, 'apps')) {
31
+ throw new Error(
32
+ `${where} lists "apps". A shell opens exactly one app: replace "apps" with "app": { id, name, urls: { <environment>: url } }.`
33
+ );
34
+ }
35
+ if (Array.isArray(shell.app)) {
36
+ throw new Error(
37
+ `${where} names ${shell.app.length} apps in "app". A shell opens exactly one app: "app" should be one { id, name, urls } object.`
38
+ );
39
+ }
40
+ if (!isObject(shell.app)) {
41
+ throw new Error(
42
+ `${where} should name the app it opens as "app": { id, name, urls: { <environment>: url } }. Received ${JSON.stringify(
43
+ shell.app
44
+ )}.`
45
+ );
46
+ }
47
+ if (typeof shell.app.id !== 'string' || shell.app.id.length === 0) {
48
+ throw new Error(
49
+ `${where} "app.id" should be a string. Received ${JSON.stringify(shell.app.id)}.`
50
+ );
51
+ }
52
+ const url = isObject(shell.app.urls) ? shell.app.urls[shell.environment] : undefined;
53
+ if (typeof url !== 'string' || url.length === 0) {
54
+ throw new Error(
55
+ `${where} "app.urls" should have a url for the shell's environment "${
56
+ shell.environment
57
+ }". Received ${JSON.stringify(shell.app.urls)}.`
58
+ );
59
+ }
60
+ if (shell.profile !== undefined && typeof shell.profile !== 'string') {
61
+ throw new Error(
62
+ `${where} "profile" should be the name of a capability profile. Received ${JSON.stringify(
63
+ shell.profile
64
+ )}.`
65
+ );
66
+ }
67
+ // The store version, major.minor.patch; `lowdefy mobile build` sets the patch.
68
+ if (shell.version !== undefined && typeof shell.version !== 'string') {
69
+ throw new Error(
70
+ `${where} "version" should be a string. Received ${JSON.stringify(shell.version)}.`
71
+ );
72
+ }
73
+ // The icon is a path relative to shell.json; `lowdefy mobile config` copies it there.
74
+ if (shell.icon !== undefined && (typeof shell.icon !== 'string' || shell.icon.length === 0)) {
75
+ throw new Error(
76
+ `${where} "icon" should be the path of a PNG, relative to the shell config. Received ${JSON.stringify(
77
+ shell.icon
78
+ )}.`
79
+ );
80
+ }
81
+ if (shell.color !== undefined && !/^#[0-9a-fA-F]{6}$/.test(shell.color)) {
82
+ throw new Error(
83
+ `${where} "color" should be a colour like "#0B5FFF". Received ${JSON.stringify(shell.color)}.`
84
+ );
85
+ }
86
+ if (shell.eas !== undefined) {
87
+ if (!isObject(shell.eas) || typeof shell.eas.projectId !== 'string') {
88
+ throw new Error(
89
+ `${where} "eas" should be { owner, projectId }, the shell's Expo account and project. Received ${JSON.stringify(
90
+ shell.eas
91
+ )}.`
92
+ );
93
+ }
94
+ if (shell.eas.owner !== undefined && typeof shell.eas.owner !== 'string') {
95
+ throw new Error(
96
+ `${where} "eas.owner" should be a string. Received ${JSON.stringify(shell.eas.owner)}.`
97
+ );
98
+ }
99
+ }
100
+ // The shell plugins `lowdefy mobile config` installs into the shell project
101
+ // (resolveShellPlugins.js).
102
+ if (shell.plugins !== undefined) {
103
+ checkPlugins({ plugins: shell.plugins, where });
104
+ }
105
+ return shell;
106
+ }
107
+
108
+ function checkPlugins({ plugins, where }) {
109
+ if (!Array.isArray(plugins)) {
110
+ throw new Error(
111
+ `${where} "plugins" should be an array of { name, version, typePrefix?, options? }. Received ${JSON.stringify(
112
+ plugins
113
+ )}.`
114
+ );
115
+ }
116
+ plugins.forEach((plugin, index) => {
117
+ const at = `${where} "plugins[${index}]"`;
118
+ if (!isObject(plugin)) {
119
+ throw new Error(
120
+ `${at} should be { name, version, typePrefix?, options? }. Received ${JSON.stringify(
121
+ plugin
122
+ )}.`
123
+ );
124
+ }
125
+ ['name', 'version'].forEach((key) => {
126
+ if (typeof plugin[key] !== 'string' || plugin[key].length === 0) {
127
+ throw new Error(
128
+ `${at} "${key}" should be a string. Received ${JSON.stringify(plugin[key])}.`
129
+ );
130
+ }
131
+ });
132
+ if (plugin.typePrefix !== undefined && typeof plugin.typePrefix !== 'string') {
133
+ throw new Error(
134
+ `${at} "typePrefix" should be a string. Received ${JSON.stringify(plugin.typePrefix)}.`
135
+ );
136
+ }
137
+ if (plugin.options !== undefined && !isObject(plugin.options)) {
138
+ throw new Error(
139
+ `${at} "options" should be an object. Received ${JSON.stringify(plugin.options)}.`
140
+ );
141
+ }
142
+ });
143
+ }
144
+
145
+ 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;