@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,205 @@
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 type manifest: every block, action and operator type this shell carries, with the plugin
19
+ package that provides it and that package's major version. A page renders only when the shell
20
+ carries every type its type set names at the same major version (src/types/checkTypeSet.js).
21
+
22
+ Made from src/plugins/shellPlugins.json, the plugin packages the shell registers
23
+ (src/plugins/types.js), and the shell's plugins (shell.json `plugins`, read with
24
+ readShellConfig.js): each one's `./types` `native.blocks` and `native.actions`, named with its
25
+ typePrefix, as src/plugins/appTypes.js registers them. A plugin without a `./types` export adds
26
+ none. Written to src/plugins/typeManifest.js, which the shell's JavaScript
27
+ imports (src/config/getShellConfig.js). The manifest travels in the JavaScript bundle, not the
28
+ Expo config, so it always matches the code that runs: a type added to the plugin maps reaches a
29
+ shell with an over-the-air update or from Metro, without a native build. The file is committed;
30
+ src/plugins/types.test.js fails when it is stale, and `pnpm release:version` rewrites it after
31
+ the version bump. `lowdefy mobile config` rewrites it in a copied shell, with that shell's
32
+ plugins.
33
+
34
+ node scripts/createTypeManifest.mjs prints the manifest as JSON
35
+ node scripts/createTypeManifest.mjs --write writes src/plugins/typeManifest.js
36
+
37
+ Usage: pnpm --filter=@lowdefy/shell type-manifest
38
+ */
39
+
40
+ import fs from 'node:fs';
41
+ import { createRequire } from 'node:module';
42
+ import path from 'node:path';
43
+ import { fileURLToPath, pathToFileURL } from 'node:url';
44
+
45
+ const shellRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
46
+ const require = createRequire(import.meta.url);
47
+ const readShellConfig = require('../readShellConfig.js');
48
+
49
+ const NATIVE_CATEGORIES = ['actions', 'blocks'];
50
+
51
+ const CATEGORIES = ['actions', 'blocks', 'operators'];
52
+
53
+ const HEADER = `/*
54
+ Copyright 2020-2026 Lowdefy, Inc
55
+
56
+ Licensed under the Apache License, Version 2.0 (the "License");
57
+ you may not use this file except in compliance with the License.
58
+ You may obtain a copy of the License at
59
+
60
+ http://www.apache.org/licenses/LICENSE-2.0
61
+
62
+ Unless required by applicable law or agreed to in writing, software
63
+ distributed under the License is distributed on an "AS IS" BASIS,
64
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
65
+ See the License for the specific language governing permissions and
66
+ limitations under the License.
67
+ */
68
+
69
+ // Generated by scripts/createTypeManifest.mjs from src/plugins/shellPlugins.json and shell.json
70
+ // \`plugins\`. Do not edit.
71
+ `;
72
+
73
+ // The plugin packages export no package.json, so it is found above the resolved types file.
74
+ function readMajorVersion(packageName) {
75
+ let directory = path.dirname(require.resolve(`${packageName}/types`));
76
+ while (!fs.existsSync(path.join(directory, 'package.json'))) {
77
+ directory = path.dirname(directory);
78
+ }
79
+ const { name, version } = JSON.parse(
80
+ fs.readFileSync(path.join(directory, 'package.json'), 'utf8')
81
+ );
82
+ if (name !== packageName) {
83
+ throw new Error(`Could not find the package.json of "${packageName}".`);
84
+ }
85
+ return Number(version.split('.')[0]);
86
+ }
87
+
88
+ function getPackageTypeNames({ category, types }) {
89
+ if (category === 'operators') {
90
+ return types.operators.client;
91
+ }
92
+ return types[category];
93
+ }
94
+
95
+ // Two packages giving one type name would leave one of them unreachable on a page.
96
+ function addType({ category, manifest, name, packageName }) {
97
+ const existing = manifest[category][name];
98
+ if (existing !== undefined && existing.package !== packageName) {
99
+ throw new Error(
100
+ `Shell plugin "${packageName}" gives the ${category.slice(0, -1)} type "${name}", which "${
101
+ existing.package
102
+ }" already gives. Set a typePrefix on the plugin in mobile.shells[].plugins.`
103
+ );
104
+ }
105
+ manifest[category][name] = { package: packageName, version: readMajorVersion(packageName) };
106
+ }
107
+
108
+ // A shell plugin need not export "./types": a native-only plugin, such as a wrapped SDK, gives no
109
+ // types. Resolving a missing export throws, so the plugin's own package.json says, found on the
110
+ // lookup paths of `require` (its exports need not list package.json).
111
+ function hasTypesExport(packageName) {
112
+ const packageJsonPath = require.resolve
113
+ .paths(packageName)
114
+ .map((directory) => path.join(directory, packageName, 'package.json'))
115
+ .find((filePath) => fs.existsSync(filePath));
116
+ if (packageJsonPath === undefined) {
117
+ throw new Error(`Shell plugin "${packageName}" is not installed.`);
118
+ }
119
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
120
+ return packageJson.exports?.['./types'] !== undefined;
121
+ }
122
+
123
+ // A shell plugin's native type names, from its `./types` `native: { blocks, actions }`.
124
+ async function addShellPluginTypes({ manifest, plugin }) {
125
+ const { default: types } = await import(pathToFileURL(require.resolve(`${plugin.name}/types`)));
126
+ NATIVE_CATEGORIES.forEach((category) => {
127
+ const names = types.native?.[category] ?? [];
128
+ if (!Array.isArray(names)) {
129
+ throw new Error(
130
+ `Shell plugin "${
131
+ plugin.name
132
+ }" "./types" native.${category} should be an array of type names. Received ${JSON.stringify(
133
+ names
134
+ )}.`
135
+ );
136
+ }
137
+ names.forEach((name) => {
138
+ addType({
139
+ category,
140
+ manifest,
141
+ name: `${plugin.typePrefix ?? ''}${name}`,
142
+ packageName: plugin.name,
143
+ });
144
+ });
145
+ });
146
+ }
147
+
148
+ function readShellPlugins() {
149
+ return JSON.parse(fs.readFileSync(path.join(shellRoot, 'src/plugins/shellPlugins.json'), 'utf8'));
150
+ }
151
+
152
+ async function createTypeManifest({
153
+ plugins = readShellPlugins(),
154
+ shellPlugins = readShellConfig().shell.plugins ?? [],
155
+ } = {}) {
156
+ const manifest = { actions: {}, blocks: {}, operators: {} };
157
+ for (const plugin of plugins) {
158
+ if (!CATEGORIES.includes(plugin.category)) {
159
+ throw new Error(
160
+ `Shell plugin "${plugin.package}" has an unknown category. Received ${JSON.stringify(
161
+ plugin.category
162
+ )}.`
163
+ );
164
+ }
165
+ const { default: types } = await import(`${plugin.package}/types`);
166
+ const packageTypeNames = getPackageTypeNames({ category: plugin.category, types });
167
+ const names = plugin.types ?? packageTypeNames;
168
+ names.forEach((name) => {
169
+ if (!packageTypeNames.includes(name)) {
170
+ throw new Error(
171
+ `Shell plugin "${plugin.package}" has no ${plugin.category} type "${name}".`
172
+ );
173
+ }
174
+ addType({ category: plugin.category, manifest, name, packageName: plugin.package });
175
+ });
176
+ }
177
+ for (const plugin of shellPlugins) {
178
+ if (hasTypesExport(plugin.name)) {
179
+ await addShellPluginTypes({ manifest, plugin });
180
+ }
181
+ }
182
+ return manifest;
183
+ }
184
+
185
+ async function createTypeManifestModule() {
186
+ const manifest = await createTypeManifest();
187
+ return `${HEADER}\nconst typeManifest = ${JSON.stringify(
188
+ manifest,
189
+ null,
190
+ 2
191
+ )};\n\nexport default typeManifest;\n`;
192
+ }
193
+
194
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
195
+ if (process.argv.includes('--write')) {
196
+ fs.writeFileSync(
197
+ path.join(shellRoot, 'src/plugins/typeManifest.js'),
198
+ await createTypeManifestModule()
199
+ );
200
+ } else {
201
+ process.stdout.write(JSON.stringify(await createTypeManifest()));
202
+ }
203
+ }
204
+
205
+ export default createTypeManifest;
package/shell.dev.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "lowdefy-dev",
3
+ "name": "Lowdefy Dev",
4
+ "profile": "dev",
5
+ "scheme": "lowdefy-dev",
6
+ "environment": "dev",
7
+ "ios": { "bundleIdentifier": "com.lowdefy.dev" },
8
+ "android": { "package": "com.lowdefy.dev" },
9
+ "store": {},
10
+ "app": { "id": "dev", "name": "Lowdefy Dev", "urls": { "dev": "http://localhost:3000" } }
11
+ }
@@ -0,0 +1,111 @@
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 { DEV_ENTRY, PROFILES, mergeProfiles, pluginName, resolveProfile } = require('./profiles.js');
18
+
19
+ const VARIANTS = ['dev', 'release'];
20
+
21
+ // The string the dev-tab client sends to the dev server. scripts/checkReleaseBundle.mjs fails when a
22
+ // release bundle holds it. Keep it in step with src/dev/tab/devClientMarker.js.
23
+ const DEV_CLIENT_MARKER = 'lowdefy-dev-tab-client';
24
+
25
+ // Everything any profile carries: every module, entry and permission a build can leave out.
26
+ const EVERYTHING = Object.keys(PROFILES).reduce(
27
+ (all, name) => mergeProfiles(all, resolveProfile(name)),
28
+ resolveProfile('basic')
29
+ );
30
+
31
+ // What the `dev` profile adds itself: the developer features, which a release build never carries.
32
+ const DEV_ONLY = PROFILES.dev;
33
+
34
+ // The shell's capability profile: shell.json `profile`, else `dev` for Lowdefy Dev and `basic` for
35
+ // any other shell.
36
+ function getProfileName({ shell, variant }) {
37
+ return shell.profile ?? (variant === 'dev' ? 'dev' : 'basic');
38
+ }
39
+
40
+ /**
41
+ * What a shell build of this variant carries: its profile, resolved (profiles.js). The dev variant
42
+ * also carries Lowdefy Dev's developer features, whatever the profile; a release build carries
43
+ * none of them.
44
+ */
45
+ function getShellProfile({ shell, variant }) {
46
+ const profile = resolveProfile(getProfileName({ shell, variant }));
47
+ if (variant === 'dev') {
48
+ return mergeProfiles(profile, { ...resolveProfile('dev'), name: profile.name });
49
+ }
50
+ return {
51
+ ...profile,
52
+ modules: Object.fromEntries(
53
+ Object.entries(profile.modules).filter(([name]) => !(name in DEV_ONLY.modules))
54
+ ),
55
+ plugins: profile.plugins.filter((plugin) => !(pluginName(plugin) in DEV_ONLY.modules)),
56
+ permissions: profile.permissions.filter(
57
+ (permission) => !DEV_ONLY.permissions.includes(permission)
58
+ ),
59
+ jsEntries: profile.jsEntries.filter((entry) => !DEV_ONLY.jsEntries.includes(entry)),
60
+ };
61
+ }
62
+
63
+ /**
64
+ * What a shell build of this variant leaves out: every module, JavaScript entry and permission some
65
+ * profile carries and its own does not. app.config.ts leaves the modules unlinked and their config
66
+ * plugins unapplied (withExcludedNativeModules.js) and blocks the permissions; metro.config.js
67
+ * resolves the entries to empty modules.
68
+ *
69
+ * @returns {{ modules: Object<string, string|null>, jsEntries: string[], permissions: string[] }}
70
+ * modules as { [package]: nativeModuleName }.
71
+ */
72
+ function getExclusions({ shell, variant }) {
73
+ const profile = getShellProfile({ shell, variant });
74
+ return {
75
+ modules: Object.fromEntries(
76
+ Object.entries(EVERYTHING.modules).filter(([name]) => !(name in profile.modules))
77
+ ),
78
+ jsEntries: EVERYTHING.jsEntries.filter((entry) => !profile.jsEntries.includes(entry)),
79
+ permissions: EVERYTHING.permissions.filter(
80
+ (permission) => !profile.permissions.includes(permission)
81
+ ),
82
+ };
83
+ }
84
+
85
+ // The native modules a shell build leaves unlinked, as { [package]: nativeModuleName }.
86
+ function getExcludedModules({ shell, variant }) {
87
+ return getExclusions({ shell, variant }).modules;
88
+ }
89
+
90
+ // LOWDEFY_SHELL_VARIANT=dev | release. Without it, Lowdefy Dev (no LOWDEFY_SHELL_CONFIG) is dev and
91
+ // every other shell is release, so a customer shell leaves the developer features out unless its
92
+ // build asks for them.
93
+ function getShellVariant(env = process.env) {
94
+ const variant = env.LOWDEFY_SHELL_VARIANT || (env.LOWDEFY_SHELL_CONFIG ? 'release' : 'dev');
95
+ if (!VARIANTS.includes(variant)) {
96
+ throw new Error(
97
+ `LOWDEFY_SHELL_VARIANT must be "dev" or "release". Received ${JSON.stringify(variant)}.`
98
+ );
99
+ }
100
+ return variant;
101
+ }
102
+
103
+ module.exports = {
104
+ DEV_CLIENT_MARKER,
105
+ DEV_ENTRY,
106
+ getExcludedModules,
107
+ getExclusions,
108
+ getProfileName,
109
+ getShellProfile,
110
+ getShellVariant,
111
+ };
@@ -0,0 +1,32 @@
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 { createContext, useContext, useSyncExternalStore } from 'react';
18
+
19
+ // { auth, session, host, locale, shell } of the app the shell has open.
20
+ const ShellContext = createContext(null);
21
+
22
+ function useShell() {
23
+ return useContext(ShellContext);
24
+ }
25
+
26
+ function useSessionState() {
27
+ const { session } = useShell();
28
+ return useSyncExternalStore(session.subscribe, session.getState);
29
+ }
30
+
31
+ export { useSessionState, useShell };
32
+ export default ShellContext;
@@ -0,0 +1,29 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { type } from '@lowdefy/helpers';
18
+
19
+ // The native body of actions-core's CopyToClipboard, with its params ({ copy }), through
20
+ // expo-clipboard (globals.clipboard).
21
+ async function CopyToClipboard({ globals, params }) {
22
+ const { copy } = params;
23
+ if (!type.isString(copy)) {
24
+ throw new Error(`CopyToClipboard requires "copy" text. Received ${JSON.stringify(copy)}.`);
25
+ }
26
+ await globals.clipboard.setString(copy);
27
+ }
28
+
29
+ export default CopyToClipboard;
@@ -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 { jest } from '@jest/globals';
18
+
19
+ import CopyToClipboard from './CopyToClipboard.js';
20
+
21
+ test('CopyToClipboard copies the text through the shell clipboard', async () => {
22
+ const clipboard = { setString: jest.fn(async () => undefined) };
23
+ await CopyToClipboard({ globals: { clipboard }, params: { copy: 'JOB-42' } });
24
+ expect(clipboard.setString.mock.calls).toEqual([['JOB-42']]);
25
+ });
26
+
27
+ test('CopyToClipboard throws when there is no text to copy', async () => {
28
+ const clipboard = { setString: jest.fn(async () => undefined) };
29
+ await expect(CopyToClipboard({ globals: { clipboard }, params: {} })).rejects.toThrow(
30
+ 'CopyToClipboard requires "copy" text. Received undefined.'
31
+ );
32
+ expect(clipboard.setString).not.toHaveBeenCalled();
33
+ });
@@ -0,0 +1,55 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { type } from '@lowdefy/helpers';
18
+
19
+ import injected from '../device/injected.js';
20
+
21
+ function numberOrNull(value) {
22
+ return type.isNumber(value) ? value : null;
23
+ }
24
+
25
+ /**
26
+ * The native body of actions-core's GeolocationCurrentPosition, answering as the web body does:
27
+ * `{ coords, timestamp }`, or `{ code: 1, error: 'PERMISSION_DENIED', message }` when location is
28
+ * refused. Until the shell carries device location (the device actions), it answers only with a
29
+ * location Lowdefy Dev injected (src/device/injected.js), and throws otherwise.
30
+ */
31
+ async function GeolocationCurrentPosition() {
32
+ if (injected('permission:location') === 'denied') {
33
+ return { code: 1, error: 'PERMISSION_DENIED', message: 'Location permission was denied.' };
34
+ }
35
+ const location = injected('location');
36
+ if (type.isUndefined(location)) {
37
+ throw new Error(
38
+ 'GeolocationCurrentPosition is not available on this phone yet: the shell has no device location. In Lowdefy Dev, inject a location.'
39
+ );
40
+ }
41
+ return {
42
+ coords: {
43
+ accuracy: numberOrNull(location.accuracy),
44
+ altitude: numberOrNull(location.altitude),
45
+ altitudeAccuracy: numberOrNull(location.altitudeAccuracy),
46
+ heading: numberOrNull(location.heading),
47
+ latitude: location.latitude,
48
+ longitude: location.longitude,
49
+ speed: numberOrNull(location.speed),
50
+ },
51
+ timestamp: location.timestamp ?? Date.now(),
52
+ };
53
+ }
54
+
55
+ export default GeolocationCurrentPosition;
@@ -0,0 +1,66 @@
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 createDeviceInjections from '../dev/inject/createDeviceInjections.js';
18
+ import createFakeNetwork from '../test/createFakeNetwork.js';
19
+ import createNetworkStatus from '../host/createNetworkStatus.js';
20
+ import GeolocationCurrentPosition from './GeolocationCurrentPosition.js';
21
+ import { installInjector } from '../device/injected.js';
22
+
23
+ let injections;
24
+ let remove;
25
+ beforeEach(() => {
26
+ injections = createDeviceInjections({
27
+ networkStatus: createNetworkStatus({ network: createFakeNetwork() }),
28
+ });
29
+ remove = installInjector(injections);
30
+ });
31
+ afterEach(() => remove());
32
+
33
+ test('GeolocationCurrentPosition answers with the injected location in the web shape', async () => {
34
+ injections.inject({
35
+ kind: 'location',
36
+ value: { latitude: -33.92, longitude: 18.42, accuracy: 12, timestamp: 1760778000000 },
37
+ });
38
+ await expect(GeolocationCurrentPosition({ params: {} })).resolves.toEqual({
39
+ coords: {
40
+ accuracy: 12,
41
+ altitude: null,
42
+ altitudeAccuracy: null,
43
+ heading: null,
44
+ latitude: -33.92,
45
+ longitude: 18.42,
46
+ speed: null,
47
+ },
48
+ timestamp: 1760778000000,
49
+ });
50
+ });
51
+
52
+ test('GeolocationCurrentPosition answers as the web does when location is refused', async () => {
53
+ injections.inject({ kind: 'location', value: { latitude: -33.92, longitude: 18.42 } });
54
+ injections.inject({ kind: 'permission', name: 'location', value: 'denied' });
55
+ await expect(GeolocationCurrentPosition({ params: {} })).resolves.toEqual({
56
+ code: 1,
57
+ error: 'PERMISSION_DENIED',
58
+ message: 'Location permission was denied.',
59
+ });
60
+ });
61
+
62
+ test('GeolocationCurrentPosition throws when the shell has no location to give', async () => {
63
+ await expect(GeolocationCurrentPosition({ params: {} })).rejects.toThrow(
64
+ 'GeolocationCurrentPosition is not available on this phone yet: the shell has no device location. In Lowdefy Dev, inject a location.'
65
+ );
66
+ });
@@ -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
+ import { type } from '@lowdefy/helpers';
18
+ import parseStoredValue from '@lowdefy/actions-core/localStorage/parseStoredValue.js';
19
+ import validateStorageKey from '@lowdefy/actions-core/localStorage/validateStorageKey.js';
20
+
21
+ // The native body of actions-core's GetLocalStorage: reads the app's key from the device store
22
+ // (expo-sqlite's key-value store), where the web reads localStorage.
23
+ function GetLocalStorage({ globals, methods, params }) {
24
+ if (!type.isObject(params)) {
25
+ throw new Error('GetLocalStorage params should be an object.');
26
+ }
27
+ const { key } = params;
28
+ validateStorageKey({ actionType: 'GetLocalStorage', key });
29
+ const item = globals.storage.get(methods.getAppStorageKey(key));
30
+ if (type.isNone(item)) {
31
+ return params.default;
32
+ }
33
+ return parseStoredValue(item);
34
+ }
35
+
36
+ export default GetLocalStorage;
@@ -0,0 +1,31 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { type } from '@lowdefy/helpers';
18
+ import validateStorageKey from '@lowdefy/actions-core/localStorage/validateStorageKey.js';
19
+
20
+ // The native body of actions-core's RemoveLocalStorage: removes the app's key from the device
21
+ // store (expo-sqlite's key-value store), where the web removes it from localStorage.
22
+ function RemoveLocalStorage({ globals, methods, params }) {
23
+ if (!type.isObject(params)) {
24
+ throw new Error('RemoveLocalStorage params should be an object.');
25
+ }
26
+ const { key } = params;
27
+ validateStorageKey({ actionType: 'RemoveLocalStorage', key });
28
+ globals.storage.remove(methods.getAppStorageKey(key));
29
+ }
30
+
31
+ export default RemoveLocalStorage;
@@ -0,0 +1,66 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { type } from '@lowdefy/helpers';
18
+
19
+ import measureInScrollView from './measureInScrollView.js';
20
+
21
+ // Where the block's top lands, by scrollIntoView's `block` option. 'nearest' needs the current
22
+ // scroll position, which a ScrollView does not report, so it scrolls as 'start' does.
23
+ function getScrollTop({ align, element, viewport }) {
24
+ switch (align) {
25
+ case 'center':
26
+ return element.top - (viewport.height - element.height) / 2;
27
+ case 'end':
28
+ return element.top - viewport.height + element.height;
29
+ default:
30
+ return element.top;
31
+ }
32
+ }
33
+
34
+ // The native body of actions-core's ScrollTo: scrolls the page's ScrollView to a block's measured
35
+ // position, or to `top` and `left` as window.scrollTo does. A block not on the page is left alone,
36
+ // as on the web.
37
+ async function ScrollTo({ globals, methods, params }) {
38
+ if (!type.isObject(params)) {
39
+ throw new Error('Invalid ScrollTo, check action params.');
40
+ }
41
+ const pageId = methods.getPageId();
42
+ const scrollView = globals.elements.find({ kind: 'scroll', pageId });
43
+ if (scrollView === null) {
44
+ return;
45
+ }
46
+ if (type.isNone(params.blockId)) {
47
+ scrollView.scrollTo({
48
+ animated: params.behavior === 'smooth',
49
+ x: params.left ?? 0,
50
+ y: params.top ?? 0,
51
+ });
52
+ return;
53
+ }
54
+ const view = globals.elements.find({ blockId: params.blockId, kind: 'view', pageId });
55
+ if (view === null) {
56
+ return;
57
+ }
58
+ const { element, viewport } = await measureInScrollView({ scrollView, view });
59
+ scrollView.scrollTo({
60
+ animated: params.options?.behavior === 'smooth',
61
+ x: 0,
62
+ y: Math.max(0, getScrollTop({ align: params.options?.block, element, viewport })),
63
+ });
64
+ }
65
+
66
+ export default ScrollTo;