@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,112 @@
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 { appStorageKey, type } from '@lowdefy/helpers';
18
+
19
+ import loadDayjsLocale from './loadDayjsLocale.js';
20
+ import resolveActiveLocale from './resolveActiveLocale.js';
21
+
22
+ /**
23
+ * The locale of one app in the shell, kept as the web client keeps it (useLocale): the app's
24
+ * i18n config from the mobile root, the person's choice that SetLocale sets and the device store
25
+ * keeps under the app's key, and the device's locale.
26
+ *
27
+ * `apply` points the app at its active locale: `lowdefy.i18n` (read by `_t`, `_locale` and
28
+ * translate), the window members the engine trace reads, and the dayjs locale `_dayjs` formats
29
+ * with. Screens call it as they render and re-render on `subscribe`.
30
+ *
31
+ * It sets `window.__lowdefy_setLocale`, which actions-core's SetLocale calls.
32
+ *
33
+ * @param {Object} params
34
+ * @param {() => string[]} params.getDeviceLocales
35
+ * @param {Object} params.globals The app's host globals.
36
+ * @param {Object} params.session The app's session (createAppSession).
37
+ * @param {Object} params.storage The device store.
38
+ */
39
+ function createShellLocale({ getDeviceLocales, globals, session, storage }) {
40
+ const listeners = new Set();
41
+ let version = 0;
42
+
43
+ function getStorageKey() {
44
+ const { rootConfig } = session.getState();
45
+ return appStorageKey({
46
+ basePath: '',
47
+ name: 'lowdefy_locale',
48
+ slug: rootConfig.lowdefyApp.slug,
49
+ });
50
+ }
51
+
52
+ // A host store may throw when the platform blocks storage, as the core allows; the choice then
53
+ // lasts only while the app runs.
54
+ function readPreference() {
55
+ try {
56
+ return storage.get(getStorageKey()) ?? 'auto';
57
+ } catch {
58
+ return 'auto';
59
+ }
60
+ }
61
+
62
+ function writePreference(preference) {
63
+ try {
64
+ if (preference === 'auto') {
65
+ storage.remove(getStorageKey());
66
+ return;
67
+ }
68
+ storage.set(getStorageKey(), preference);
69
+ } catch {
70
+ // See readPreference.
71
+ }
72
+ }
73
+
74
+ function apply() {
75
+ const { lowdefy, rootConfig } = session.getState();
76
+ const { i18n } = rootConfig;
77
+ if (type.isNone(i18n.defaultLocale)) {
78
+ return;
79
+ }
80
+ const active = resolveActiveLocale({
81
+ deviceLocales: getDeviceLocales(),
82
+ i18n,
83
+ preference: readPreference(),
84
+ });
85
+ lowdefy.i18n = { ...i18n, active };
86
+ globals.window.__lowdefy_locale = active;
87
+ globals.window.__lowdefy_supported_locales = i18n.locales;
88
+ const dayjsId = i18n.locales.find((locale) => locale.code === active)?.dayjs;
89
+ if (type.isString(dayjsId)) {
90
+ loadDayjsLocale(dayjsId);
91
+ }
92
+ }
93
+
94
+ // Applied now, not on the next render: SetLocale updates the engine straight after.
95
+ function setPreference(code) {
96
+ writePreference(code ?? 'auto');
97
+ apply();
98
+ version += 1;
99
+ listeners.forEach((listener) => listener());
100
+ }
101
+
102
+ function subscribe(listener) {
103
+ listeners.add(listener);
104
+ return () => listeners.delete(listener);
105
+ }
106
+
107
+ globals.window.__lowdefy_setLocale = setPreference;
108
+
109
+ return { apply, getVersion: () => version, setPreference, subscribe };
110
+ }
111
+
112
+ export default createShellLocale;
@@ -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
+ import { jest } from '@jest/globals';
18
+
19
+ import createMemoryStore from '../host/createMemoryStore.js';
20
+ import createShellLocale from './createShellLocale.js';
21
+
22
+ const i18n = {
23
+ defaultLocale: 'en-US',
24
+ locales: [
25
+ { code: 'en-US', dayjs: 'en' },
26
+ { code: 'af-ZA', dayjs: 'af' },
27
+ ],
28
+ messages: {},
29
+ };
30
+
31
+ function createLocale(rootI18n) {
32
+ const lowdefy = {};
33
+ const session = {
34
+ getState: () => ({ lowdefy, rootConfig: { i18n: rootI18n, lowdefyApp: { slug: 'field' } } }),
35
+ };
36
+ const globals = { window: {} };
37
+ const storage = createMemoryStore();
38
+ const locale = createShellLocale({
39
+ getDeviceLocales: () => ['en-ZA'],
40
+ globals,
41
+ session,
42
+ storage,
43
+ });
44
+ return { globals, locale, lowdefy, storage };
45
+ }
46
+
47
+ test('createShellLocale points lowdefy.i18n and the window at the active locale', () => {
48
+ const { globals, locale, lowdefy } = createLocale(i18n);
49
+ locale.apply();
50
+ expect(lowdefy.i18n).toEqual({ ...i18n, active: 'en-US' });
51
+ expect(globals.window.__lowdefy_locale).toBe('en-US');
52
+ expect(globals.window.__lowdefy_supported_locales).toBe(i18n.locales);
53
+ });
54
+
55
+ test('SetLocale sets the choice through the window, keeps it and tells the screens', () => {
56
+ const { globals, locale, lowdefy, storage } = createLocale(i18n);
57
+ const listener = jest.fn();
58
+ locale.subscribe(listener);
59
+ globals.window.__lowdefy_setLocale('af-ZA');
60
+ expect(lowdefy.i18n.active).toBe('af-ZA');
61
+ expect(storage.get('lowdefy:field::lowdefy_locale')).toBe('af-ZA');
62
+ expect(listener).toHaveBeenCalledTimes(1);
63
+ expect(locale.getVersion()).toBe(1);
64
+ globals.window.__lowdefy_setLocale('auto');
65
+ expect(storage.keys()).toEqual([]);
66
+ expect(lowdefy.i18n.active).toBe('en-US');
67
+ });
68
+
69
+ test('createShellLocale leaves an app without i18n alone', () => {
70
+ const { globals, locale, lowdefy } = createLocale({});
71
+ locale.apply();
72
+ expect(lowdefy.i18n).toBeUndefined();
73
+ expect(globals.window.__lowdefy_locale).toBeUndefined();
74
+ });
@@ -0,0 +1,165 @@
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
+ // Generated by scripts/createDayjsLocales.mjs from the dayjs package the shell carries. Do not edit.
18
+
19
+ const dayjsLocales = {
20
+ af: () => require('dayjs/locale/af.js'),
21
+ am: () => require('dayjs/locale/am.js'),
22
+ ar: () => require('dayjs/locale/ar.js'),
23
+ 'ar-dz': () => require('dayjs/locale/ar-dz.js'),
24
+ 'ar-iq': () => require('dayjs/locale/ar-iq.js'),
25
+ 'ar-kw': () => require('dayjs/locale/ar-kw.js'),
26
+ 'ar-ly': () => require('dayjs/locale/ar-ly.js'),
27
+ 'ar-ma': () => require('dayjs/locale/ar-ma.js'),
28
+ 'ar-sa': () => require('dayjs/locale/ar-sa.js'),
29
+ 'ar-tn': () => require('dayjs/locale/ar-tn.js'),
30
+ az: () => require('dayjs/locale/az.js'),
31
+ be: () => require('dayjs/locale/be.js'),
32
+ bg: () => require('dayjs/locale/bg.js'),
33
+ bi: () => require('dayjs/locale/bi.js'),
34
+ bm: () => require('dayjs/locale/bm.js'),
35
+ bn: () => require('dayjs/locale/bn.js'),
36
+ 'bn-bd': () => require('dayjs/locale/bn-bd.js'),
37
+ bo: () => require('dayjs/locale/bo.js'),
38
+ br: () => require('dayjs/locale/br.js'),
39
+ bs: () => require('dayjs/locale/bs.js'),
40
+ ca: () => require('dayjs/locale/ca.js'),
41
+ cs: () => require('dayjs/locale/cs.js'),
42
+ cv: () => require('dayjs/locale/cv.js'),
43
+ cy: () => require('dayjs/locale/cy.js'),
44
+ da: () => require('dayjs/locale/da.js'),
45
+ de: () => require('dayjs/locale/de.js'),
46
+ 'de-at': () => require('dayjs/locale/de-at.js'),
47
+ 'de-ch': () => require('dayjs/locale/de-ch.js'),
48
+ dv: () => require('dayjs/locale/dv.js'),
49
+ el: () => require('dayjs/locale/el.js'),
50
+ en: () => require('dayjs/locale/en.js'),
51
+ 'en-au': () => require('dayjs/locale/en-au.js'),
52
+ 'en-ca': () => require('dayjs/locale/en-ca.js'),
53
+ 'en-gb': () => require('dayjs/locale/en-gb.js'),
54
+ 'en-ie': () => require('dayjs/locale/en-ie.js'),
55
+ 'en-il': () => require('dayjs/locale/en-il.js'),
56
+ 'en-in': () => require('dayjs/locale/en-in.js'),
57
+ 'en-nz': () => require('dayjs/locale/en-nz.js'),
58
+ 'en-sg': () => require('dayjs/locale/en-sg.js'),
59
+ 'en-tt': () => require('dayjs/locale/en-tt.js'),
60
+ eo: () => require('dayjs/locale/eo.js'),
61
+ es: () => require('dayjs/locale/es.js'),
62
+ 'es-do': () => require('dayjs/locale/es-do.js'),
63
+ 'es-mx': () => require('dayjs/locale/es-mx.js'),
64
+ 'es-pr': () => require('dayjs/locale/es-pr.js'),
65
+ 'es-us': () => require('dayjs/locale/es-us.js'),
66
+ et: () => require('dayjs/locale/et.js'),
67
+ eu: () => require('dayjs/locale/eu.js'),
68
+ fa: () => require('dayjs/locale/fa.js'),
69
+ fi: () => require('dayjs/locale/fi.js'),
70
+ fo: () => require('dayjs/locale/fo.js'),
71
+ fr: () => require('dayjs/locale/fr.js'),
72
+ 'fr-ca': () => require('dayjs/locale/fr-ca.js'),
73
+ 'fr-ch': () => require('dayjs/locale/fr-ch.js'),
74
+ fy: () => require('dayjs/locale/fy.js'),
75
+ ga: () => require('dayjs/locale/ga.js'),
76
+ gd: () => require('dayjs/locale/gd.js'),
77
+ gl: () => require('dayjs/locale/gl.js'),
78
+ 'gom-latn': () => require('dayjs/locale/gom-latn.js'),
79
+ gu: () => require('dayjs/locale/gu.js'),
80
+ he: () => require('dayjs/locale/he.js'),
81
+ hi: () => require('dayjs/locale/hi.js'),
82
+ hr: () => require('dayjs/locale/hr.js'),
83
+ ht: () => require('dayjs/locale/ht.js'),
84
+ hu: () => require('dayjs/locale/hu.js'),
85
+ 'hy-am': () => require('dayjs/locale/hy-am.js'),
86
+ id: () => require('dayjs/locale/id.js'),
87
+ is: () => require('dayjs/locale/is.js'),
88
+ it: () => require('dayjs/locale/it.js'),
89
+ 'it-ch': () => require('dayjs/locale/it-ch.js'),
90
+ ja: () => require('dayjs/locale/ja.js'),
91
+ jv: () => require('dayjs/locale/jv.js'),
92
+ ka: () => require('dayjs/locale/ka.js'),
93
+ kk: () => require('dayjs/locale/kk.js'),
94
+ km: () => require('dayjs/locale/km.js'),
95
+ kn: () => require('dayjs/locale/kn.js'),
96
+ ko: () => require('dayjs/locale/ko.js'),
97
+ ku: () => require('dayjs/locale/ku.js'),
98
+ ky: () => require('dayjs/locale/ky.js'),
99
+ lb: () => require('dayjs/locale/lb.js'),
100
+ lo: () => require('dayjs/locale/lo.js'),
101
+ lt: () => require('dayjs/locale/lt.js'),
102
+ lv: () => require('dayjs/locale/lv.js'),
103
+ me: () => require('dayjs/locale/me.js'),
104
+ mi: () => require('dayjs/locale/mi.js'),
105
+ mk: () => require('dayjs/locale/mk.js'),
106
+ ml: () => require('dayjs/locale/ml.js'),
107
+ mn: () => require('dayjs/locale/mn.js'),
108
+ mr: () => require('dayjs/locale/mr.js'),
109
+ ms: () => require('dayjs/locale/ms.js'),
110
+ 'ms-my': () => require('dayjs/locale/ms-my.js'),
111
+ mt: () => require('dayjs/locale/mt.js'),
112
+ my: () => require('dayjs/locale/my.js'),
113
+ nb: () => require('dayjs/locale/nb.js'),
114
+ ne: () => require('dayjs/locale/ne.js'),
115
+ nl: () => require('dayjs/locale/nl.js'),
116
+ 'nl-be': () => require('dayjs/locale/nl-be.js'),
117
+ nn: () => require('dayjs/locale/nn.js'),
118
+ 'oc-lnc': () => require('dayjs/locale/oc-lnc.js'),
119
+ 'pa-in': () => require('dayjs/locale/pa-in.js'),
120
+ pl: () => require('dayjs/locale/pl.js'),
121
+ pt: () => require('dayjs/locale/pt.js'),
122
+ 'pt-br': () => require('dayjs/locale/pt-br.js'),
123
+ rn: () => require('dayjs/locale/rn.js'),
124
+ ro: () => require('dayjs/locale/ro.js'),
125
+ ru: () => require('dayjs/locale/ru.js'),
126
+ rw: () => require('dayjs/locale/rw.js'),
127
+ sd: () => require('dayjs/locale/sd.js'),
128
+ se: () => require('dayjs/locale/se.js'),
129
+ si: () => require('dayjs/locale/si.js'),
130
+ sk: () => require('dayjs/locale/sk.js'),
131
+ sl: () => require('dayjs/locale/sl.js'),
132
+ sq: () => require('dayjs/locale/sq.js'),
133
+ sr: () => require('dayjs/locale/sr.js'),
134
+ 'sr-cyrl': () => require('dayjs/locale/sr-cyrl.js'),
135
+ ss: () => require('dayjs/locale/ss.js'),
136
+ sv: () => require('dayjs/locale/sv.js'),
137
+ 'sv-fi': () => require('dayjs/locale/sv-fi.js'),
138
+ sw: () => require('dayjs/locale/sw.js'),
139
+ ta: () => require('dayjs/locale/ta.js'),
140
+ te: () => require('dayjs/locale/te.js'),
141
+ tet: () => require('dayjs/locale/tet.js'),
142
+ tg: () => require('dayjs/locale/tg.js'),
143
+ th: () => require('dayjs/locale/th.js'),
144
+ tk: () => require('dayjs/locale/tk.js'),
145
+ 'tl-ph': () => require('dayjs/locale/tl-ph.js'),
146
+ tlh: () => require('dayjs/locale/tlh.js'),
147
+ tr: () => require('dayjs/locale/tr.js'),
148
+ tzl: () => require('dayjs/locale/tzl.js'),
149
+ tzm: () => require('dayjs/locale/tzm.js'),
150
+ 'tzm-latn': () => require('dayjs/locale/tzm-latn.js'),
151
+ 'ug-cn': () => require('dayjs/locale/ug-cn.js'),
152
+ uk: () => require('dayjs/locale/uk.js'),
153
+ ur: () => require('dayjs/locale/ur.js'),
154
+ uz: () => require('dayjs/locale/uz.js'),
155
+ 'uz-latn': () => require('dayjs/locale/uz-latn.js'),
156
+ vi: () => require('dayjs/locale/vi.js'),
157
+ 'x-pseudo': () => require('dayjs/locale/x-pseudo.js'),
158
+ yo: () => require('dayjs/locale/yo.js'),
159
+ zh: () => require('dayjs/locale/zh.js'),
160
+ 'zh-cn': () => require('dayjs/locale/zh-cn.js'),
161
+ 'zh-hk': () => require('dayjs/locale/zh-hk.js'),
162
+ 'zh-tw': () => require('dayjs/locale/zh-tw.js'),
163
+ };
164
+
165
+ export default dayjsLocales;
@@ -0,0 +1,23 @@
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 device's locale, which Hermes' Intl reads from the platform (iOS's NSLocale, Android's
18
+ // default Locale).
19
+ function getDeviceLocales() {
20
+ return [Intl.DateTimeFormat().resolvedOptions().locale];
21
+ }
22
+
23
+ export default getDeviceLocales;
@@ -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 dayjs from 'dayjs';
18
+
19
+ import dayjsLocales from './dayjsLocales.js';
20
+
21
+ // Makes a dayjs locale the one _dayjs formats with, loading its data the first time. The dayjs
22
+ // instance is the one @lowdefy/operators-dayjs imports. An id dayjs does not ship leaves the
23
+ // locale as it is, as on the web.
24
+ function loadDayjsLocale(id) {
25
+ if (!Object.hasOwn(dayjsLocales, id)) {
26
+ return;
27
+ }
28
+ dayjsLocales[id]();
29
+ dayjs.locale(id);
30
+ }
31
+
32
+ export default loadDayjsLocale;
@@ -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 first device locale the app supports, by exact code, then by language.
18
+ function pickBest({ candidates, supported }) {
19
+ for (const candidate of candidates) {
20
+ if (supported.includes(candidate)) return candidate;
21
+ const language = candidate.split('-')[0];
22
+ const match = supported.find((code) => code.split('-')[0] === language);
23
+ if (match) return match;
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ /**
29
+ * The app's active locale, as the web client picks it (useLocale): the person's choice (SetLocale)
30
+ * when the app supports it, else the device's locale the app supports best, else the app's
31
+ * default. Undefined for an app without i18n.
32
+ *
33
+ * @param {Object} params
34
+ * @param {string[]} params.deviceLocales The device's locales, preferred first.
35
+ * @param {Object} params.i18n The app's i18n config: `{ defaultLocale, locales }`.
36
+ * @param {string} params.preference A locale code, or 'auto'.
37
+ */
38
+ function resolveActiveLocale({ deviceLocales, i18n, preference }) {
39
+ const supported = (i18n.locales ?? []).map((locale) => locale.code);
40
+ if (preference !== 'auto' && supported.includes(preference)) {
41
+ return preference;
42
+ }
43
+ return pickBest({ candidates: deviceLocales, supported }) ?? i18n.defaultLocale;
44
+ }
45
+
46
+ export default resolveActiveLocale;
@@ -0,0 +1,40 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import resolveActiveLocale from './resolveActiveLocale.js';
18
+
19
+ const i18n = {
20
+ defaultLocale: 'en-US',
21
+ locales: [{ code: 'en-US' }, { code: 'af-ZA' }, { code: 'pt-BR' }],
22
+ };
23
+
24
+ test('resolveActiveLocale takes the person choice when the app supports it', () => {
25
+ expect(resolveActiveLocale({ deviceLocales: ['pt-BR'], i18n, preference: 'af-ZA' })).toBe(
26
+ 'af-ZA'
27
+ );
28
+ expect(resolveActiveLocale({ deviceLocales: ['pt-BR'], i18n, preference: 'de-DE' })).toBe(
29
+ 'pt-BR'
30
+ );
31
+ });
32
+
33
+ test('resolveActiveLocale matches the device locale by code, then by language', () => {
34
+ expect(resolveActiveLocale({ deviceLocales: ['pt-BR'], i18n, preference: 'auto' })).toBe('pt-BR');
35
+ expect(resolveActiveLocale({ deviceLocales: ['pt-PT'], i18n, preference: 'auto' })).toBe('pt-BR');
36
+ });
37
+
38
+ test('resolveActiveLocale falls back to the app default', () => {
39
+ expect(resolveActiveLocale({ deviceLocales: ['ja-JP'], i18n, preference: 'auto' })).toBe('en-US');
40
+ });
@@ -0,0 +1,24 @@
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 } from 'react';
18
+
19
+ // The index of the frame group (app/(t0,t1,t2,t3,t4)) a screen lives in, which its group layout
20
+ // provides from the segment Expo Router gives it. A screen cannot read it from useSegments: those
21
+ // are the focused route's, so every tab rendered at once would take the focused tab's index.
22
+ const SlotContext = createContext(0);
23
+
24
+ export default SlotContext;
@@ -0,0 +1,27 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // The route groups the frame's screens live in: app/(t0,t1,t2,t3,t4) is one set of routes shared
18
+ // by five groups. With a tabs frame each tab is a group, with its own stack; mobile.menus.tabs has
19
+ // at most five items. A drawer or stack frame uses the first group only.
20
+ const FRAME_SLOTS = ['t0', 't1', 't2', 't3', 't4'];
21
+
22
+ function getSlotIndex(segment) {
23
+ return FRAME_SLOTS.indexOf(String(segment).replace(/^\(|\)$/g, ''));
24
+ }
25
+
26
+ export { getSlotIndex };
27
+ export default FRAME_SLOTS;
@@ -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
+ // The frame an app's mobile menus declare: { kind: 'tabs', items }, { kind: 'drawer', items } or
18
+ // { kind: 'stack' }. The server drops the items a caller may not open, so a tabs frame left with
19
+ // fewer than two items, or a drawer with none, falls back to a stack.
20
+ function getFrame(menus) {
21
+ if ((menus?.tabs?.length ?? 0) >= 2) {
22
+ return { kind: 'tabs', items: menus.tabs };
23
+ }
24
+ if ((menus?.drawer?.length ?? 0) >= 1) {
25
+ return { kind: 'drawer', items: menus.drawer };
26
+ }
27
+ return { kind: 'stack', items: [] };
28
+ }
29
+
30
+ export default getFrame;
@@ -0,0 +1,63 @@
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
+ function noop() {}
20
+
21
+ /**
22
+ * The header options a Screen page's config gives before the page renders, in the shape a Screen
23
+ * reports (client-native screen/Screen.js): the parts that size the bar (a large title, a search
24
+ * field) and how it is drawn (a backgroundTint's clear bar), from literal properties. The Screen
25
+ * reports its full options once it renders; these set the bar first, so the page's scroll view is
26
+ * first laid out under the bar it will have. iOS keeps a large title collapsed when the page's
27
+ * scroll view was laid out under a standard bar before the large title came.
28
+ *
29
+ * @param {Object} params
30
+ * @param {Object} params.pageConfig The page config, as the server sends it.
31
+ * @returns {Object|null} The options, or null for a page that is not a Screen.
32
+ */
33
+ function getInitialScreenOptions({ pageConfig }) {
34
+ if (pageConfig.type !== 'Screen') {
35
+ return null;
36
+ }
37
+ const properties = pageConfig.properties ?? {};
38
+ const headerSearch = properties.headerSearch;
39
+ return {
40
+ backgroundTint: !type.isNone(properties.backgroundTint),
41
+ headerActions: [],
42
+ headerBottom: false,
43
+ headerLeft: undefined,
44
+ headerRight: undefined,
45
+ headerSearch: type.isObject(headerSearch)
46
+ ? {
47
+ autoFocus: false,
48
+ hideWhenScrolling: headerSearch.hideWhenScrolling === true,
49
+ onCancel: noop,
50
+ onChangeText: noop,
51
+ onSearch: noop,
52
+ placeholder: type.isString(headerSearch.placeholder)
53
+ ? headerSearch.placeholder
54
+ : undefined,
55
+ }
56
+ : undefined,
57
+ largeTitle: properties.largeTitle === true,
58
+ presentation: type.isString(properties.presentation) ? properties.presentation : 'card',
59
+ title: type.isString(properties.title) ? properties.title : undefined,
60
+ };
61
+ }
62
+
63
+ export default getInitialScreenOptions;