@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,73 @@
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 fs = require('node:fs');
18
+ const path = require('node:path');
19
+ const { getDefaultConfig } = require('expo/metro-config');
20
+
21
+ const readShellConfig = require('./readShellConfig.js');
22
+ const { getExclusions, getShellVariant } = require('./shellVariant.js');
23
+
24
+ const projectRoot = __dirname;
25
+
26
+ function findWorkspaceRoot(directory) {
27
+ if (fs.existsSync(path.join(directory, 'pnpm-workspace.yaml'))) {
28
+ return directory;
29
+ }
30
+ const parent = path.dirname(directory);
31
+ return parent === directory ? null : findWorkspaceRoot(parent);
32
+ }
33
+
34
+ const config = getDefaultConfig(projectRoot);
35
+ config.resolver.unstable_enableSymlinks = true;
36
+
37
+ // A shell resolves the feature modules its capability profile leaves out to empty ones
38
+ // (getExclusions in shellVariant.js). A release shell (LOWDEFY_SHELL_VARIANT=release, the default
39
+ // for every shell but Lowdefy Dev) leaves out the developer features: the dev-tab client, connect
40
+ // flow and shake menu are not in its bundle (scripts/checkReleaseBundle.mjs checks it).
41
+ const excludedEntries = new Set(
42
+ getExclusions({ shell: readShellConfig().shell, variant: getShellVariant() }).jsEntries
43
+ );
44
+
45
+ // Lowdefy packages shared with the server import a few Node built-ins in code the client never
46
+ // runs (@lowdefy/errors reads build maps with `path`). The web client's bundler leaves them empty
47
+ // in the browser; Metro does the same here.
48
+ const SERVER_ONLY_BUILTINS = new Set(['path']);
49
+ const resolveDefault = config.resolver.resolveRequest;
50
+ config.resolver.resolveRequest = (context, moduleName, platform) => {
51
+ if (SERVER_ONLY_BUILTINS.has(moduleName)) {
52
+ return { type: 'empty' };
53
+ }
54
+ const resolution = (resolveDefault ?? context.resolveRequest)(context, moduleName, platform);
55
+ if (resolution.type === 'sourceFile' && excludedEntries.has(resolution.filePath)) {
56
+ return { type: 'empty' };
57
+ }
58
+ return resolution;
59
+ };
60
+
61
+ // In a pnpm workspace (the Lowdefy monorepo) the shell's Lowdefy packages are symlinks to their
62
+ // workspace folders: Metro watches the whole workspace and resolves from the shell's own
63
+ // node_modules before the workspace root's.
64
+ const workspaceRoot = findWorkspaceRoot(path.dirname(projectRoot));
65
+ if (workspaceRoot !== null) {
66
+ config.watchFolders = [workspaceRoot];
67
+ config.resolver.nodeModulesPaths = [
68
+ path.join(projectRoot, 'node_modules'),
69
+ path.join(workspaceRoot, 'node_modules'),
70
+ ];
71
+ }
72
+
73
+ module.exports = config;
@@ -0,0 +1,12 @@
1
+ {
2
+ "nativeVersion": 2,
3
+ "hash": "32a477d7fe75d305f87e57d1f633f2fe6d3f9a59",
4
+ "profiles": {
5
+ "basic": "cd8736936deaf720f1427407d9401b1f900a9d75",
6
+ "field": "5375e8ffcc85dfb3a9c0f9abc0b7ee6cd1dfeea4",
7
+ "field-nfc": "5375e8ffcc85dfb3a9c0f9abc0b7ee6cd1dfeea4",
8
+ "field-tracking": "5375e8ffcc85dfb3a9c0f9abc0b7ee6cd1dfeea4",
9
+ "dev": "654adfb2adfcd0c77034176aa02ac36c20a449c2",
10
+ "development": "654adfb2adfcd0c77034176aa02ac36c20a449c2"
11
+ }
12
+ }
package/package.json ADDED
@@ -0,0 +1,130 @@
1
+ {
2
+ "name": "@lowdefy/shell",
3
+ "version": "0.0.0-experimental-20261019052246",
4
+ "license": "Apache-2.0",
5
+ "description": "Lowdefy Shell - the Expo app every Lowdefy phone app is built from",
6
+ "homepage": "https://lowdefy.com",
7
+ "keywords": [
8
+ "lowdefy",
9
+ "react native",
10
+ "expo"
11
+ ],
12
+ "bugs": {
13
+ "url": "https://github.com/lowdefy/lowdefy/issues"
14
+ },
15
+ "contributors": [
16
+ {
17
+ "name": "Sam Tolmay",
18
+ "url": "https://github.com/SamTolmay"
19
+ },
20
+ {
21
+ "name": "Gerrie van Wyk",
22
+ "url": "https://github.com/Gervwyk"
23
+ }
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/lowdefy/lowdefy.git"
28
+ },
29
+ "main": "expo-router/entry",
30
+ "nativeVersion": 2,
31
+ "files": [
32
+ "app/*",
33
+ "scripts/*",
34
+ "src/*",
35
+ "app.config.ts",
36
+ "applyShellPlugins.js",
37
+ "authCallbackIntentFilters.js",
38
+ "babel.config.js",
39
+ "checkShellConfig.js",
40
+ "checkShellPlugins.js",
41
+ "getPrivacyManifest.js",
42
+ "metro.config.js",
43
+ "native-fingerprint.json",
44
+ "profiles.js",
45
+ "readShellConfig.js",
46
+ "resolveShellPlugins.js",
47
+ "runtimeVersion.js",
48
+ "shell.dev.json",
49
+ "shellVariant.js",
50
+ "withExcludedNativeModules.js",
51
+ "!**/*.test.*",
52
+ "!scripts/checkNativeVersion.mjs"
53
+ ],
54
+ "dependencies": {
55
+ "@expo/ui": "58.0.20",
56
+ "@lowdefy/actions-core": "0.0.0-experimental-20261019052246",
57
+ "@lowdefy/blocks-native": "0.0.0-experimental-20261019052246",
58
+ "@lowdefy/client-core": "0.0.0-experimental-20261019052246",
59
+ "@lowdefy/client-native": "0.0.0-experimental-20261019052246",
60
+ "@lowdefy/engine": "0.0.0-experimental-20261019052246",
61
+ "@lowdefy/helpers": "0.0.0-experimental-20261019052246",
62
+ "@lowdefy/node-utils": "0.0.0-experimental-20261019052246",
63
+ "@lowdefy/operators-change-case": "0.0.0-experimental-20261019052246",
64
+ "@lowdefy/operators-cron": "0.0.0-experimental-20261019052246",
65
+ "@lowdefy/operators-dayjs": "0.0.0-experimental-20261019052246",
66
+ "@lowdefy/operators-diff": "0.0.0-experimental-20261019052246",
67
+ "@lowdefy/operators-js": "0.0.0-experimental-20261019052246",
68
+ "@lowdefy/operators-jsonata": "0.0.0-experimental-20261019052246",
69
+ "@lowdefy/operators-mql": "0.0.0-experimental-20261019052246",
70
+ "@lowdefy/operators-nunjucks": "0.0.0-experimental-20261019052246",
71
+ "@lowdefy/operators-uuid": "0.0.0-experimental-20261019052246",
72
+ "@lowdefy/operators-yaml": "0.0.0-experimental-20261019052246",
73
+ "dayjs": "1.11.20",
74
+ "expo": "58.0.14",
75
+ "expo-clipboard": "58.0.3",
76
+ "expo-camera": "58.0.10",
77
+ "expo-constants": "58.0.12",
78
+ "expo-crypto": "58.0.7",
79
+ "expo-dev-client": "58.0.14",
80
+ "expo-font": "58.0.8",
81
+ "expo-image": "58.0.15",
82
+ "expo-linking": "58.0.13",
83
+ "expo-local-authentication": "58.0.2",
84
+ "expo-network": "58.0.3",
85
+ "expo-router": "58.0.23",
86
+ "expo-secure-store": "58.0.2",
87
+ "expo-sensors": "58.0.4",
88
+ "expo-speech-recognition": "58.0.0",
89
+ "expo-splash-screen": "58.0.10",
90
+ "expo-sqlite": "58.0.13",
91
+ "expo-updates": "58.0.17",
92
+ "expo-web-browser": "58.0.6",
93
+ "jimp-compact": "0.16.1",
94
+ "react": "19.3.0",
95
+ "react-native": "0.88.0",
96
+ "react-native-safe-area-context": "5.10.1",
97
+ "react-native-screens": "4.30.0",
98
+ "react-native-svg": "15.16.0",
99
+ "react-native-view-shot": "6.1.1"
100
+ },
101
+ "devDependencies": {
102
+ "@babel/core": "7.29.6",
103
+ "@babel/plugin-transform-class-static-block": "7.29.7",
104
+ "@expo/fingerprint": "0.21.5",
105
+ "@jest/globals": "29.7.0",
106
+ "@lowdefy/build": "0.0.0-experimental-20261019052246",
107
+ "@lowdefy/logger": "0.0.0-experimental-20261019052246",
108
+ "@react-native/babel-preset": "0.88.0",
109
+ "@react-native/jest-preset": "0.88.0",
110
+ "@testing-library/react-native": "14.1.0",
111
+ "babel-jest": "29.7.0",
112
+ "babel-preset-expo": "58.0.17",
113
+ "jest": "29.7.0",
114
+ "test-renderer": "1.3.1"
115
+ },
116
+ "publishConfig": {
117
+ "access": "public"
118
+ },
119
+ "scripts": {
120
+ "dayjs-locales": "node scripts/createDayjsLocales.mjs",
121
+ "check:native-version": "node scripts/checkNativeVersion.mjs",
122
+ "check:release-bundle": "node scripts/checkReleaseBundle.mjs",
123
+ "export:android": "expo export --platform android --output-dir dist/android",
124
+ "export:ios": "expo export --platform ios --output-dir dist/ios",
125
+ "start": "expo start",
126
+ "test": "jest && node --test fixtures/hello.test.mjs scripts/createDayjsLocales.test.mjs checkShellConfig.test.mjs checkShellPlugins.test.mjs profiles.test.mjs resolveShellPlugins.test.mjs runtimeVersion.test.mjs shellVariant.test.mjs storeCompliance.test.mjs authCallbackIntentFilters.test.mjs scripts/checkNativeVersion.test.mjs scripts/createTypeManifest.test.mjs",
127
+ "shell-version": "node scripts/createShellVersion.mjs --write && prettier --config ../../.prettierrc --write src/version/shellVersion.js",
128
+ "type-manifest": "node scripts/createTypeManifest.mjs --write && prettier --config ../../.prettierrc --write src/plugins/typeManifest.js"
129
+ }
130
+ }
package/profiles.js ADDED
@@ -0,0 +1,274 @@
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 path = require('node:path');
18
+
19
+ /*
20
+ The capability profiles a shell picks from (shell.json `profile`). A shell cannot pick modules one
21
+ by one: every shell on one profile carries the same native modules, so a fleet of shells has a
22
+ handful of native variants to test, and a shell carries no permission text or store declaration
23
+ for a feature its profile does not have.
24
+
25
+ Each entry lists what the profile adds to the one it `extends`:
26
+ - capabilities: the device features it gives the app (shell.json `capabilities`).
27
+ - modules: { [package]: nativeModuleName }, the native modules it links. The name is the one the
28
+ module's JavaScript asks for, which scripts/checkReleaseBundle.mjs looks for in a bundle that
29
+ should not carry it; null for a module with no JavaScript of its own in the bundle.
30
+ - plugins: the config plugin entries app.config.ts applies. A profile's entry for a plugin its
31
+ parent also has replaces the parent's.
32
+ - permissions: the iOS Info.plist keys and Android permissions its modules ask for.
33
+ - jsEntries: the feature modules only it uses. A bundle for any other profile resolves them to
34
+ empty modules (metro.config.js).
35
+ - privacyReasons: the required-reason APIs its modules use, for the iOS privacy manifest, as
36
+ { category, reasons } (Apple's NSPrivacyAccessedAPIType and NSPrivacyAccessedAPITypeReasons).
37
+ - dataCollected: the data its features let the app collect, for the App Store privacy label and
38
+ Google Play's data safety form, as { type, purpose }.
39
+
40
+ Every native module the shell package depends on is in some profile. A build leaves out every one
41
+ not in its profile (getExclusions in shellVariant.js), and a release build also leaves out the
42
+ developer features (`dev`'s own entries), whatever its profile.
43
+ */
44
+
45
+ // Lowdefy Dev's developer features: connect, shake menu and dev-tab client (src/dev).
46
+ const DEV_ENTRY = path.join(__dirname, 'src', 'dev', 'index.js');
47
+
48
+ // One microphone purpose text for every plugin that writes NSMicrophoneUsageDescription: speech
49
+ // (basic) and expo-camera (field, dev). expo-camera writes its own default when no earlier plugin
50
+ // set the key, so it is given the same text and plugin order cannot change what a shell shows.
51
+ const MICROPHONE_PERMISSION =
52
+ '$(PRODUCT_NAME) uses the microphone when you tap the microphone button, to turn what you say into text.';
53
+ const SPEECH_RECOGNITION_PERMISSION =
54
+ '$(PRODUCT_NAME) turns what you say into text on this device. Your voice is not recorded or sent anywhere.';
55
+
56
+ const PROFILES = {
57
+ // What every shell carries: the runtime, native blocks, sign-in, biometric unlock and on-device
58
+ // dictation (expo-speech-recognition, for the Composer and AgentChat mic).
59
+ basic: {
60
+ capabilities: ['biometrics', 'sign-in', 'speech'],
61
+ modules: {
62
+ '@expo/ui': 'ExpoUI',
63
+ 'expo-clipboard': 'ExpoClipboard',
64
+ 'expo-constants': 'ExponentConstants',
65
+ 'expo-crypto': 'ExpoCrypto',
66
+ 'expo-font': 'ExpoFontLoader',
67
+ 'expo-image': 'ExpoImage',
68
+ 'expo-linking': 'ExpoLinking',
69
+ 'expo-local-authentication': 'ExpoLocalAuthentication',
70
+ 'expo-network': 'ExpoNetwork',
71
+ 'expo-router': null,
72
+ 'expo-secure-store': 'ExpoSecureStore',
73
+ 'expo-speech-recognition': 'ExpoSpeechRecognition',
74
+ 'expo-splash-screen': 'ExpoSplashScreen',
75
+ 'expo-sqlite': 'ExpoSQLite',
76
+ 'expo-updates': 'ExpoUpdates',
77
+ 'expo-web-browser': 'ExpoWebBrowser',
78
+ 'react-native-safe-area-context': 'RNCSafeAreaContext',
79
+ 'react-native-screens': 'RNSModule',
80
+ 'react-native-svg': 'RNSVGSvgViewModule',
81
+ },
82
+ plugins: [
83
+ 'expo-router',
84
+ [
85
+ 'expo-local-authentication',
86
+ { faceIDPermission: 'Allow $(PRODUCT_NAME) to use Face ID to unlock the app.' },
87
+ ],
88
+ [
89
+ 'expo-secure-store',
90
+ { faceIDPermission: 'Allow $(PRODUCT_NAME) to use Face ID to unlock the app.' },
91
+ ],
92
+ [
93
+ 'expo-speech-recognition',
94
+ {
95
+ microphonePermission: MICROPHONE_PERMISSION,
96
+ speechRecognitionPermission: SPEECH_RECOGNITION_PERMISSION,
97
+ },
98
+ ],
99
+ 'expo-splash-screen',
100
+ 'expo-sqlite',
101
+ 'expo-web-browser',
102
+ ],
103
+ permissions: [
104
+ 'NSFaceIDUsageDescription',
105
+ 'android.permission.USE_BIOMETRIC',
106
+ 'NSMicrophoneUsageDescription',
107
+ 'NSSpeechRecognitionUsageDescription',
108
+ 'android.permission.RECORD_AUDIO',
109
+ ],
110
+ jsEntries: [],
111
+ // React Native's own (file timestamps, UserDefaults, boot time), and the free disk space
112
+ // expo-sqlite checks before it writes.
113
+ privacyReasons: [
114
+ { category: 'NSPrivacyAccessedAPICategoryFileTimestamp', reasons: ['C617.1'] },
115
+ { category: 'NSPrivacyAccessedAPICategoryUserDefaults', reasons: ['CA92.1'] },
116
+ { category: 'NSPrivacyAccessedAPICategorySystemBootTime', reasons: ['35F9.1'] },
117
+ { category: 'NSPrivacyAccessedAPICategoryDiskSpace', reasons: ['E174.1'] },
118
+ ],
119
+ dataCollected: [
120
+ { type: 'emailAddress', purpose: 'appFunctionality' },
121
+ { type: 'userId', purpose: 'appFunctionality' },
122
+ ],
123
+ },
124
+ // Field work: the camera and scanning. Photos, location, maps and signatures join it as the
125
+ // shell gains them.
126
+ field: {
127
+ extends: 'basic',
128
+ capabilities: ['camera'],
129
+ modules: { 'expo-camera': 'ExpoCamera' },
130
+ // The camera does not add Android's microphone permission: the shell takes no video.
131
+ // RECORD_AUDIO comes from basic, for speech.
132
+ plugins: [
133
+ [
134
+ 'expo-camera',
135
+ {
136
+ cameraPermission: 'Allow $(PRODUCT_NAME) to use the camera.',
137
+ microphonePermission: MICROPHONE_PERMISSION,
138
+ recordAudioAndroid: false,
139
+ },
140
+ ],
141
+ ],
142
+ permissions: ['NSCameraUsageDescription', 'android.permission.CAMERA'],
143
+ dataCollected: [{ type: 'photos', purpose: 'appFunctionality' }],
144
+ },
145
+ // `field` and NFC, which joins it as the shell gains it.
146
+ 'field-nfc': {
147
+ extends: 'field',
148
+ },
149
+ // `field` and background location and geofences, which join it as the shell gains them. Its
150
+ // session token stays readable while the phone is locked (src/auth/isBackgroundShell.js).
151
+ 'field-tracking': {
152
+ extends: 'field',
153
+ },
154
+ // Lowdefy Dev: `field` and the developer features, which read the code `lowdefy dev` prints with
155
+ // the camera, open the dev menu on a shake (expo-sensors) and take screenshots for the dev tab
156
+ // (react-native-view-shot). expo-dev-client lets a development build load a local bundle.
157
+ dev: {
158
+ extends: 'field',
159
+ capabilities: [],
160
+ modules: {
161
+ 'expo-dev-client': null,
162
+ 'expo-sensors': 'ExponentAccelerometer',
163
+ 'react-native-view-shot': 'RNViewShot',
164
+ },
165
+ plugins: [
166
+ [
167
+ 'expo-camera',
168
+ {
169
+ cameraPermission: 'Allow $(PRODUCT_NAME) to scan the code lowdefy dev prints.',
170
+ microphonePermission: MICROPHONE_PERMISSION,
171
+ recordAudioAndroid: false,
172
+ },
173
+ ],
174
+ ],
175
+ permissions: ['NSMotionUsageDescription'],
176
+ jsEntries: [DEV_ENTRY],
177
+ },
178
+ // A development build of a developer's own shell (`lowdefy mobile build --profile development`):
179
+ // Lowdefy Dev's features, for apps whose shell plugins Lowdefy Dev does not carry.
180
+ development: {
181
+ extends: 'dev',
182
+ },
183
+ };
184
+
185
+ // The profiles a store or testing build of an app's shell picks from.
186
+ const RELEASE_PROFILES = ['basic', 'field', 'field-nfc', 'field-tracking'];
187
+
188
+ function pluginName(plugin) {
189
+ return Array.isArray(plugin) ? plugin[0] : plugin;
190
+ }
191
+
192
+ function unique(values) {
193
+ return [...new Set(values)];
194
+ }
195
+
196
+ // Merges two resolved profiles, or a profile entry onto its resolved parent: lists are joined
197
+ // without duplicates, and `added`'s modules and plugins replace `base`'s of the same name.
198
+ function mergeProfiles(base, added) {
199
+ const plugins = [...base.plugins];
200
+ (added.plugins ?? []).forEach((plugin) => {
201
+ const index = plugins.findIndex((existing) => pluginName(existing) === pluginName(plugin));
202
+ if (index === -1) {
203
+ plugins.push(plugin);
204
+ } else {
205
+ plugins[index] = plugin;
206
+ }
207
+ });
208
+ const privacyReasons = base.privacyReasons.map((reason) => ({ ...reason }));
209
+ (added.privacyReasons ?? []).forEach(({ category, reasons }) => {
210
+ const existing = privacyReasons.find((reason) => reason.category === category);
211
+ if (existing) {
212
+ existing.reasons = unique([...existing.reasons, ...reasons]);
213
+ } else {
214
+ privacyReasons.push({ category, reasons: [...reasons] });
215
+ }
216
+ });
217
+ const dataCollected = [...base.dataCollected];
218
+ (added.dataCollected ?? []).forEach((data) => {
219
+ if (!dataCollected.some(({ type }) => type === data.type)) {
220
+ dataCollected.push(data);
221
+ }
222
+ });
223
+ return {
224
+ name: added.name ?? base.name,
225
+ capabilities: unique([...base.capabilities, ...(added.capabilities ?? [])]),
226
+ modules: { ...base.modules, ...added.modules },
227
+ plugins,
228
+ permissions: unique([...base.permissions, ...(added.permissions ?? [])]),
229
+ jsEntries: unique([...base.jsEntries, ...(added.jsEntries ?? [])]),
230
+ privacyReasons,
231
+ dataCollected,
232
+ };
233
+ }
234
+
235
+ const EMPTY_PROFILE = {
236
+ capabilities: [],
237
+ modules: {},
238
+ plugins: [],
239
+ permissions: [],
240
+ jsEntries: [],
241
+ privacyReasons: [],
242
+ dataCollected: [],
243
+ };
244
+
245
+ /**
246
+ * Everything a capability profile carries, with the profiles it extends folded in.
247
+ *
248
+ * @param {string} name The profile's name (shell.json `profile`).
249
+ * @returns {{ name, capabilities, modules, plugins, permissions, jsEntries, privacyReasons,
250
+ * dataCollected }}
251
+ */
252
+ function resolveProfile(name) {
253
+ if (!Object.prototype.hasOwnProperty.call(PROFILES, name)) {
254
+ throw new Error(
255
+ `Shell profile ${JSON.stringify(name)} does not exist. Use one of ${Object.keys(
256
+ PROFILES
257
+ ).join(', ')}.`
258
+ );
259
+ }
260
+ const entry = PROFILES[name];
261
+ const base = entry.extends ? resolveProfile(entry.extends) : EMPTY_PROFILE;
262
+ return mergeProfiles(base, { ...entry, name });
263
+ }
264
+
265
+ module.exports = {
266
+ DEV_ENTRY,
267
+ MICROPHONE_PERMISSION,
268
+ PROFILES,
269
+ RELEASE_PROFILES,
270
+ mergeProfiles,
271
+ pluginName,
272
+ resolveProfile,
273
+ SPEECH_RECOGNITION_PERMISSION,
274
+ };
@@ -0,0 +1,36 @@
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 fs = require('node:fs');
18
+ const path = require('node:path');
19
+
20
+ const checkShellConfig = require('./checkShellConfig.js');
21
+
22
+ // The shell's config comes from shell.json, written for each shell at build: LOWDEFY_SHELL_CONFIG
23
+ // is its path. Without it the shell is Lowdefy Dev (shell.dev.json), which opens `lowdefy dev`.
24
+ // app.config.ts, metro.config.js and scripts/checkReleaseBundle.mjs read it here.
25
+ function readShellConfig(env = process.env) {
26
+ const configPath = env.LOWDEFY_SHELL_CONFIG
27
+ ? path.resolve(env.LOWDEFY_SHELL_CONFIG)
28
+ : path.join(__dirname, 'shell.dev.json');
29
+ const shell = checkShellConfig({
30
+ configPath,
31
+ shell: JSON.parse(fs.readFileSync(configPath, 'utf8')),
32
+ });
33
+ return { configPath, shell };
34
+ }
35
+
36
+ module.exports = readShellConfig;
@@ -0,0 +1,109 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ /*
18
+ A shell's plugins (shell.json `plugins`): npm packages that add native code beyond the shell's
19
+ capability profile (profiles.js). Each is a Lowdefy shell plugin, which exports:
20
+
21
+ - `./shell` (CommonJS): an entry shaped like one row of the profiles table, which app.config.ts
22
+ folds into the shell's resolved profile (applyShellPlugins.js):
23
+ - capabilities: the device features it gives the app.
24
+ - modules: { [package]: nativeModuleName }, the native modules it links. Never one
25
+ @lowdefy/shell carries itself (checkShellPlugins.js).
26
+ - plugins: the Expo config plugin entries it applies, or a function of the plugin's `options`
27
+ (shell.json `plugins[].options`) that returns them.
28
+ - permissions: the iOS Info.plist keys and Android permissions its modules ask for.
29
+ - privacyReasons: the required-reason APIs its modules use, as { category, reasons }.
30
+ - dataCollected: the data it lets the app collect, as { type, purpose }.
31
+ - `./native` (optional): its native types, as named exports `blocks` and `actions`, each
32
+ { [typeName]: implementation }. `lowdefy mobile config` writes src/plugins/appTypes.js in the
33
+ copied shell to import them, with the plugin's `typePrefix` applied (prefixTypes.js), and
34
+ src/plugins/types.js registers them after the built-in types.
35
+ - `./types` (optional): its type names as other plugins give them, with its native ones under
36
+ `native: { blocks, actions, blockMetas, icons }`. scripts/createTypeManifest.mjs adds
37
+ `native.blocks` and `native.actions` to the type manifest.
38
+
39
+ Its native modules and `expo` are optional peerDependencies, so the web server's install leaves
40
+ them out and `lowdefy mobile config` adds them to the shell project.
41
+ */
42
+
43
+ function isObject(value) {
44
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
45
+ }
46
+
47
+ const LIST_KEYS = ['capabilities', 'permissions', 'privacyReasons', 'dataCollected'];
48
+
49
+ function loadEntry({ name, require }) {
50
+ try {
51
+ return require(`${name}/shell`);
52
+ } catch (error) {
53
+ throw new Error(
54
+ `Shell plugin "${name}" could not be loaded from "${name}/shell". A shell plugin exports "./shell", the native modules, config plugins, permissions, privacy reasons and data it brings. ${error.message}`,
55
+ { cause: error }
56
+ );
57
+ }
58
+ }
59
+
60
+ function checkEntry({ entry, name }) {
61
+ const where = `Shell plugin "${name}" "./shell"`;
62
+ if (!isObject(entry)) {
63
+ throw new Error(`${where} should export an object. Received ${JSON.stringify(entry)}.`);
64
+ }
65
+ LIST_KEYS.forEach((key) => {
66
+ if (entry[key] !== undefined && !Array.isArray(entry[key])) {
67
+ throw new Error(
68
+ `${where} "${key}" should be an array. Received ${JSON.stringify(entry[key])}.`
69
+ );
70
+ }
71
+ });
72
+ if (entry.modules !== undefined && !isObject(entry.modules)) {
73
+ throw new Error(
74
+ `${where} "modules" should be { [package]: nativeModuleName }. Received ${JSON.stringify(
75
+ entry.modules
76
+ )}.`
77
+ );
78
+ }
79
+ if (!Array.isArray(entry.plugins)) {
80
+ throw new Error(
81
+ `${where} "plugins" should be an array of config plugin entries, or a function of the plugin's options that returns one. Received ${JSON.stringify(
82
+ entry.plugins
83
+ )}.`
84
+ );
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Loads the `./shell` entry of each plugin a shell lists, with a `plugins` function called with the
90
+ * plugin's options.
91
+ *
92
+ * @param {Object} params
93
+ * @param {Object} params.shell The shell config (shell.json).
94
+ * @param {Function} params.require Resolves the plugin packages: the shell project's own, so
95
+ * `lowdefy mobile` resolves them from the copied project.
96
+ * @returns {{ name: string, version: string, typePrefix?: string, options?: Object, entry: Object }[]}
97
+ */
98
+ function resolveShellPlugins({ shell, require }) {
99
+ return (shell.plugins ?? []).map(({ name, version, typePrefix, options }) => {
100
+ const loaded = loadEntry({ name, require });
101
+ const plugins =
102
+ typeof loaded?.plugins === 'function' ? loaded.plugins(options ?? {}) : loaded?.plugins ?? [];
103
+ const entry = isObject(loaded) ? { ...loaded, plugins } : loaded;
104
+ checkEntry({ entry, name });
105
+ return { name, version, typePrefix, options, entry };
106
+ });
107
+ }
108
+
109
+ module.exports = resolveShellPlugins;