@lowdefy/shell 3.23.3 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. package/LICENSE +183 -183
  2. package/README.md +364 -2
  3. package/app/(t0,t1,t2,t3,t4)/[...path].js +38 -0
  4. package/app/(t0,t1,t2,t3,t4)/_layout.js +48 -0
  5. package/app/(t0,t1,t2,t3,t4)/index.js +38 -0
  6. package/app/+native-intent.js +39 -0
  7. package/app/_layout.js +36 -0
  8. package/app.config.ts +183 -0
  9. package/appLinks.js +65 -0
  10. package/applyShellPlugins.js +43 -0
  11. package/authCallbackIntentFilters.js +46 -0
  12. package/babel.config.js +25 -0
  13. package/checkShellConfig.js +172 -0
  14. package/checkShellPlugins.js +74 -0
  15. package/getPrivacyManifest.js +39 -0
  16. package/metro.config.js +73 -0
  17. package/native/LowdefyAppShortcuts.swift.template +84 -0
  18. package/native/withAppShortcuts.js +60 -0
  19. package/native-fingerprint.json +16 -0
  20. package/package.json +120 -40
  21. package/profiles.js +607 -0
  22. package/readShellConfig.js +36 -0
  23. package/resolveShellPlugins.js +109 -0
  24. package/runtimeVersion.js +112 -0
  25. package/scripts/checkReleaseBundle.mjs +358 -0
  26. package/scripts/createDayjsLocales.mjs +67 -0
  27. package/scripts/createShellAssets.mjs +54 -0
  28. package/scripts/createShellVersion.mjs +83 -0
  29. package/scripts/createTypeManifest.mjs +205 -0
  30. package/shell.dev.json +11 -0
  31. package/shellVariant.js +126 -0
  32. package/src/ShellContext.js +32 -0
  33. package/src/actions/CopyToClipboard.js +29 -0
  34. package/src/actions/CopyToClipboard.test.js +33 -0
  35. package/src/actions/GeolocationCurrentPosition.js +61 -0
  36. package/src/actions/GeolocationCurrentPosition.test.js +79 -0
  37. package/src/actions/GetLocalStorage.js +36 -0
  38. package/src/actions/RemoveLocalStorage.js +31 -0
  39. package/src/actions/ScrollTo.js +66 -0
  40. package/src/actions/ScrollTo.test.js +83 -0
  41. package/src/actions/SetFocus.js +33 -0
  42. package/src/actions/SetFocus.test.js +49 -0
  43. package/src/actions/SetLocalStorage.js +34 -0
  44. package/src/actions/localStorage.test.js +68 -0
  45. package/src/actions/measureInScrollView.js +36 -0
  46. package/src/actions/nativeActions.js +41 -0
  47. package/src/auth/AuthButton.js +44 -0
  48. package/src/auth/AuthGate.js +57 -0
  49. package/src/auth/AuthLink.js +43 -0
  50. package/src/auth/LockScreen.js +67 -0
  51. package/src/auth/LockScreen.test.js +37 -0
  52. package/src/auth/SignInScreen.js +268 -0
  53. package/src/auth/SignInScreen.test.js +187 -0
  54. package/src/auth/createAppAuth.js +325 -0
  55. package/src/auth/createAppAuth.test.js +424 -0
  56. package/src/auth/createAuthApi.js +115 -0
  57. package/src/auth/createAuthApi.test.js +138 -0
  58. package/src/auth/createCoreAuth.js +96 -0
  59. package/src/auth/createCoreAuth.test.js +188 -0
  60. package/src/auth/createDeviceKey.js +62 -0
  61. package/src/auth/createDeviceKey.test.js +54 -0
  62. package/src/auth/createSessionStores.js +45 -0
  63. package/src/auth/createTokenStore.js +103 -0
  64. package/src/auth/createTokenStore.test.js +116 -0
  65. package/src/auth/getAuthSettings.js +24 -0
  66. package/src/auth/getAuthView.js +39 -0
  67. package/src/auth/getAuthView.test.js +52 -0
  68. package/src/auth/getSheetReturnUrl.js +35 -0
  69. package/src/auth/getTokenStoreOptions.js +46 -0
  70. package/src/auth/isBackgroundShell.js +26 -0
  71. package/src/auth/isSheetReturnLink.js +35 -0
  72. package/src/auth/parseQuery.js +39 -0
  73. package/src/auth/purgeAppData.js +80 -0
  74. package/src/auth/purgeAppData.test.js +104 -0
  75. package/src/auth/shouldLock.js +26 -0
  76. package/src/auth/signInWithSheet.js +97 -0
  77. package/src/auth/signInWithSheet.test.js +205 -0
  78. package/src/auth/styles.js +78 -0
  79. package/src/auth/unregisterPushToken.js +62 -0
  80. package/src/auth/unregisterPushToken.test.js +67 -0
  81. package/src/auth/usesBiometricUnlock.js +23 -0
  82. package/src/auth/watchSession.js +47 -0
  83. package/src/auth/withoutCookies.js +26 -0
  84. package/src/cache/createSqliteCacheStore.js +113 -0
  85. package/src/cache/createSqliteCacheStore.test.js +93 -0
  86. package/src/cache/getCacheKey.js +25 -0
  87. package/src/cache/getLastCacheKeyName.js +22 -0
  88. package/src/components/AppProvider.js +118 -0
  89. package/src/components/Frame.js +116 -0
  90. package/src/components/Frame.test.js +101 -0
  91. package/src/components/LoadingScreen.js +35 -0
  92. package/src/components/MenuButton.js +121 -0
  93. package/src/components/MenuButton.test.js +74 -0
  94. package/src/components/MessageScreen.js +51 -0
  95. package/src/components/NavigationTheme.js +33 -0
  96. package/src/components/NoConnectionBanner.js +51 -0
  97. package/src/components/NoConnectionBanner.test.js +73 -0
  98. package/src/components/OtherPageScreen.js +95 -0
  99. package/src/components/OtherPageScreen.test.js +74 -0
  100. package/src/components/PageScreen.js +254 -0
  101. package/src/components/PageScreen.test.js +492 -0
  102. package/src/components/RouteErrorScreen.js +47 -0
  103. package/src/components/ShellRoot.js +34 -0
  104. package/src/components/ShellRoot.test.js +62 -0
  105. package/src/components/ShellThemeProvider.js +45 -0
  106. package/src/components/SwitchToTab.js +36 -0
  107. package/src/components/SwitchToTab.test.js +63 -0
  108. package/src/components/TabIconImages.js +72 -0
  109. package/src/components/UpdateScreen.js +40 -0
  110. package/src/components/getNavigationTheme.js +44 -0
  111. package/src/components/getNavigationTheme.test.js +41 -0
  112. package/src/components/styles.js +50 -0
  113. package/src/components/useSurfaceTokens.js +47 -0
  114. package/src/config/getAppApiBase.js +34 -0
  115. package/src/config/getShellConfig.js +37 -0
  116. package/src/config/getShellConfig.test.js +80 -0
  117. package/src/createShellApp.js +126 -0
  118. package/src/dev/DevShell.js +239 -0
  119. package/src/dev/DevShell.test.js +233 -0
  120. package/src/dev/connect/ConfirmSheet.js +71 -0
  121. package/src/dev/connect/ConnectScreen.js +117 -0
  122. package/src/dev/connect/QrScanner.js +89 -0
  123. package/src/dev/connection/createDevShellConfig.js +47 -0
  124. package/src/dev/connection/createDevTransport.js +51 -0
  125. package/src/dev/connection/createDevTransport.test.js +87 -0
  126. package/src/dev/connection/createServerList.js +48 -0
  127. package/src/dev/connection/isLoopbackHost.js +25 -0
  128. package/src/dev/connection/pairedShellApp.test.js +104 -0
  129. package/src/dev/connection/parseDevLink.js +66 -0
  130. package/src/dev/connection/parseDevLink.test.js +97 -0
  131. package/src/dev/connection/parseQuery.js +33 -0
  132. package/src/dev/connection/parseServerInput.js +40 -0
  133. package/src/dev/connection/parseServerUrl.js +45 -0
  134. package/src/dev/devStyles.js +97 -0
  135. package/src/dev/index.js +21 -0
  136. package/src/dev/inject/createDeviceInjections.js +188 -0
  137. package/src/dev/inject/createDeviceInjections.test.js +224 -0
  138. package/src/dev/inject/createOfflineFetch.js +35 -0
  139. package/src/dev/inject/createOfflineFetch.test.js +68 -0
  140. package/src/dev/inject/injectPush.js +60 -0
  141. package/src/dev/inject/injectShare.js +55 -0
  142. package/src/dev/inject/withInjectedDevice.js +98 -0
  143. package/src/dev/menu/BlockGallery.js +33 -0
  144. package/src/dev/menu/DevMenu.js +121 -0
  145. package/src/dev/menu/createLogBuffer.js +83 -0
  146. package/src/dev/menu/watchShake.js +52 -0
  147. package/src/dev/menu/watchShake.test.js +61 -0
  148. package/src/dev/recorder/createNativeRecorder.js +204 -0
  149. package/src/dev/recorder/createNativeRecorder.test.js +307 -0
  150. package/src/dev/redirectDevLink.js +30 -0
  151. package/src/dev/redirectDevLink.test.js +49 -0
  152. package/src/dev/steps/createStepRunner.js +184 -0
  153. package/src/dev/steps/createStepRunner.test.js +262 -0
  154. package/src/dev/steps/findBlock.js +78 -0
  155. package/src/dev/steps/isContextSettled.js +37 -0
  156. package/src/dev/steps/waitForSettle.js +39 -0
  157. package/src/dev/steps/waitUntil.js +40 -0
  158. package/src/dev/tab/DevTabClient.js +97 -0
  159. package/src/dev/tab/answerEval.js +39 -0
  160. package/src/dev/tab/answerInspect.js +45 -0
  161. package/src/dev/tab/createDevTabClient.js +272 -0
  162. package/src/dev/tab/createDevTabClient.test.js +317 -0
  163. package/src/dev/tab/describeTabError.js +35 -0
  164. package/src/dev/tab/devClientMarker.js +22 -0
  165. package/src/dev/tab/devClientMarker.test.js +23 -0
  166. package/src/dev/tab/findPageContext.js +43 -0
  167. package/src/dev/tab/getShownPage.js +42 -0
  168. package/src/dev/tab/readEventStream.js +68 -0
  169. package/src/dev/tab/reloadApp.js +42 -0
  170. package/src/dev/tab/reloadApp.test.js +115 -0
  171. package/src/device/DeviceHosts.js +32 -0
  172. package/src/device/basic/createFilesFeature.js +41 -0
  173. package/src/device/basic/createHapticsFeature.js +46 -0
  174. package/src/device/basic/createKeepAwakeFeature.js +37 -0
  175. package/src/device/basic/createPhotosFeature.js +40 -0
  176. package/src/device/basic/createRequestQueueFeature.js +37 -0
  177. package/src/device/basic/createShareFeature.js +63 -0
  178. package/src/device/basic/createShareFeature.test.js +47 -0
  179. package/src/device/basic/createSpeechFeature.js +43 -0
  180. package/src/device/basic/createUploadFeature.js +41 -0
  181. package/src/device/basic/toImagePickerOptions.js +41 -0
  182. package/src/device/basic/toPickedFile.js +54 -0
  183. package/src/device/calendar/index.js +68 -0
  184. package/src/device/contacts/index.js +67 -0
  185. package/src/device/createDevice.js +65 -0
  186. package/src/device/device.js +27 -0
  187. package/src/device/field/MapView.js +93 -0
  188. package/src/device/field/ScanCodeSheet.js +86 -0
  189. package/src/device/field/ScannerView.js +52 -0
  190. package/src/device/field/createAudioFeature.js +70 -0
  191. package/src/device/field/createBrightnessFeature.js +40 -0
  192. package/src/device/field/createCameraFeature.js +56 -0
  193. package/src/device/field/createDocumentScannerFeature.js +51 -0
  194. package/src/device/field/createLocationFeature.js +39 -0
  195. package/src/device/field/createScanRequests.js +55 -0
  196. package/src/device/field/index.js +39 -0
  197. package/src/device/field/scanRequests.js +22 -0
  198. package/src/device/injected.js +60 -0
  199. package/src/device/injected.test.js +39 -0
  200. package/src/device/nfc/index.js +89 -0
  201. package/src/device/nfc/toNfcRecord.js +44 -0
  202. package/src/device/nfc/toNfcRecord.test.js +48 -0
  203. package/src/device/tracking/createBacklog.js +63 -0
  204. package/src/device/tracking/createBacklog.test.js +47 -0
  205. package/src/device/tracking/index.js +211 -0
  206. package/src/device/tracking/index.test.js +110 -0
  207. package/src/host/createDeviceStore.js +32 -0
  208. package/src/host/createMemoryStore.js +32 -0
  209. package/src/host/createNetworkStatus.js +94 -0
  210. package/src/host/createNetworkStatus.test.js +94 -0
  211. package/src/host/createRouterAdapter.js +33 -0
  212. package/src/host/createShellFetch.js +62 -0
  213. package/src/host/createShellFetch.test.js +165 -0
  214. package/src/host/createShellGlobals.js +62 -0
  215. package/src/host/createShellHost.js +83 -0
  216. package/src/host/createShellHost.test.js +36 -0
  217. package/src/host/getOrigin.js +31 -0
  218. package/src/host/isNetworkError.js +24 -0
  219. package/src/host/isOnline.js +22 -0
  220. package/src/host/networkStatus.js +24 -0
  221. package/src/host/readServerError.js +45 -0
  222. package/src/host/readServerError.test.js +85 -0
  223. package/src/host/reportShellError.js +47 -0
  224. package/src/host/reportShellError.test.js +79 -0
  225. package/src/host/withNetworkErrors.js +38 -0
  226. package/src/host/withNetworkErrors.test.js +61 -0
  227. package/src/host/withShellHeader.js +25 -0
  228. package/src/host/withShellHeader.test.js +28 -0
  229. package/src/js/evaluateJsEntries.js +43 -0
  230. package/src/js/evaluateJsEntries.test.js +48 -0
  231. package/src/locale/createShellLocale.js +112 -0
  232. package/src/locale/createShellLocale.test.js +74 -0
  233. package/src/locale/dayjsLocales.js +165 -0
  234. package/src/locale/getDeviceLocales.js +23 -0
  235. package/src/locale/loadDayjsLocale.js +32 -0
  236. package/src/locale/resolveActiveLocale.js +46 -0
  237. package/src/locale/resolveActiveLocale.test.js +40 -0
  238. package/src/native/backgroundFetch.js +97 -0
  239. package/src/native/callEndpoint.js +63 -0
  240. package/src/native/createAppEventTrigger.js +70 -0
  241. package/src/native/currentApp.js +49 -0
  242. package/src/native/getPagePath.js +32 -0
  243. package/src/native/openPath.js +55 -0
  244. package/src/native/openPath.test.js +39 -0
  245. package/src/native/parseShellLink.js +38 -0
  246. package/src/native/parseShellLink.test.js +29 -0
  247. package/src/native/startAppServices.js +94 -0
  248. package/src/native/startAppServices.test.js +97 -0
  249. package/src/native/startShareIn.js +59 -0
  250. package/src/native/startShortcuts.js +88 -0
  251. package/src/native/startShortcuts.test.js +59 -0
  252. package/src/native/toSharePayload.js +47 -0
  253. package/src/native/toSharePayload.test.js +47 -0
  254. package/src/native/whenAppReady.js +48 -0
  255. package/src/navigation/SlotContext.js +24 -0
  256. package/src/navigation/frameSlots.js +27 -0
  257. package/src/navigation/getFrame.js +30 -0
  258. package/src/navigation/getInitialScreenOptions.js +63 -0
  259. package/src/navigation/getInitialScreenOptions.test.js +66 -0
  260. package/src/navigation/getPresentation.js +28 -0
  261. package/src/navigation/getSlotRootPath.js +27 -0
  262. package/src/navigation/getStackOptions.js +185 -0
  263. package/src/navigation/getTabHref.js +45 -0
  264. package/src/navigation/getTabIcons.js +43 -0
  265. package/src/navigation/isPageUnderHeader.js +46 -0
  266. package/src/navigation/navigation.test.js +342 -0
  267. package/src/navigation/toAppLinkPath.js +43 -0
  268. package/src/navigation/toAppLinkPath.test.js +53 -0
  269. package/src/navigation/toRouterHref.js +29 -0
  270. package/src/notifications/createNotificationsFeature.js +61 -0
  271. package/src/notifications/getCategoryAction.js +31 -0
  272. package/src/notifications/handleNotificationResponse.js +82 -0
  273. package/src/notifications/registerPushToken.js +71 -0
  274. package/src/notifications/startNotifications.js +114 -0
  275. package/src/notifications/toCategoryActions.js +42 -0
  276. package/src/notifications/toNotificationEvent.js +36 -0
  277. package/src/notifications/toScheduleRequest.js +62 -0
  278. package/src/plugins/appTypes.js +28 -0
  279. package/src/plugins/prefixTypes.js +28 -0
  280. package/src/plugins/shellPlugins.json +15 -0
  281. package/src/plugins/typeManifest.js +789 -0
  282. package/src/plugins/types.js +64 -0
  283. package/src/plugins/types.test.js +173 -0
  284. package/src/polyfills/installGetRandomValues.js +28 -0
  285. package/src/queue/createRequestQueue.js +198 -0
  286. package/src/queue/createRequestQueue.test.js +215 -0
  287. package/src/queue/createSqliteQueueStore.js +121 -0
  288. package/src/queue/summarizeQueue.js +39 -0
  289. package/src/session/createAppSession.js +323 -0
  290. package/src/session/createAppSession.test.js +391 -0
  291. package/src/session/getPageView.js +59 -0
  292. package/src/session/getPageView.test.js +137 -0
  293. package/src/sync/encodePagePath.js +27 -0
  294. package/src/sync/fetchPage.js +72 -0
  295. package/src/sync/fetchPage.test.js +84 -0
  296. package/src/sync/fetchRoot.js +43 -0
  297. package/src/sync/fetchUser.js +33 -0
  298. package/src/sync/runLimited.js +36 -0
  299. package/src/sync/toRootConfig.js +35 -0
  300. package/src/test/createFakeNetwork.js +40 -0
  301. package/src/test/createFakeSecureStore.js +40 -0
  302. package/src/test/createFakeServer.js +98 -0
  303. package/src/test/createMemoryCacheStore.js +59 -0
  304. package/src/test/createNetworkError.js +24 -0
  305. package/src/test/expoImage.js +42 -0
  306. package/src/test/expoNotifications.js +40 -0
  307. package/src/test/expoQuickActions.js +22 -0
  308. package/src/test/expoSpeechRecognition.js +31 -0
  309. package/src/test/expoTaskManager.js +25 -0
  310. package/src/test/expoUi.js +55 -0
  311. package/src/test/expoUiBottomSheet.js +64 -0
  312. package/src/test/expoUiDateTimePicker.js +26 -0
  313. package/src/test/expoUiMenu.js +46 -0
  314. package/src/test/expoUiSegmentedControl.js +47 -0
  315. package/src/test/expoUiSwiftUi.js +62 -0
  316. package/src/test/expoUiSwiftUiModifiers.js +23 -0
  317. package/src/test/expoWebBrowser.js +23 -0
  318. package/src/test/fixtures.js +98 -0
  319. package/src/test/nativeModule.js +33 -0
  320. package/src/test/renderPageScreen.js +109 -0
  321. package/src/types/checkTypeSet.js +39 -0
  322. package/src/types/collectPageTypes.js +49 -0
  323. package/src/types/collectPageTypes.test.js +90 -0
  324. package/src/types/findMissingPageTypes.js +40 -0
  325. package/src/version/compareVersions.js +38 -0
  326. package/src/version/isShellTooOld.js +30 -0
  327. package/src/version/shellVersion.js +21 -0
  328. package/src/version/shellVersion.test.js +32 -0
  329. package/src/widgets/fetchWidgetData.js +59 -0
  330. package/src/widgets/index.js +156 -0
  331. package/src/widgets/refreshWidgets.js +31 -0
  332. package/withExcludedNativeModules.js +93 -0
  333. package/dist/location.js +0 -33
  334. package/dist/public/apple-touch-icon.png +0 -0
  335. package/dist/public/icon-32.png +0 -0
  336. package/dist/public/icon-512.png +0 -0
  337. package/dist/public/icon.svg +0 -17
  338. package/dist/public/logo-dark-theme.png +0 -0
  339. package/dist/public/logo-light-theme.png +0 -0
  340. package/dist/public/logo-square-dark-theme.png +0 -0
  341. package/dist/public/logo-square-light-theme.png +0 -0
  342. package/dist/public/manifest.webmanifest +0 -16
  343. package/dist/shell/246.93432ff68186aae6c87b.js +0 -2
  344. package/dist/shell/246.93432ff68186aae6c87b.js.LICENSE.txt +0 -8
  345. package/dist/shell/454.b597ca21366afda000af.js +0 -2
  346. package/dist/shell/454.b597ca21366afda000af.js.LICENSE.txt +0 -14
  347. package/dist/shell/484.d645e6c09079f45ce044.js +0 -1
  348. package/dist/shell/706.a66931e02a549fd269bd.js +0 -2
  349. package/dist/shell/706.a66931e02a549fd269bd.js.LICENSE.txt +0 -8
  350. package/dist/shell/9.78be8c1b6c3986f404fc.js +0 -2
  351. package/dist/shell/9.78be8c1b6c3986f404fc.js.LICENSE.txt +0 -45
  352. package/dist/shell/922.43b7a3ebe22ef05b3049.js +0 -2
  353. package/dist/shell/922.43b7a3ebe22ef05b3049.js.LICENSE.txt +0 -14
  354. package/dist/shell/index.html +0 -57
  355. package/dist/shell/main.f01501a75909f38ccc32.js +0 -1
  356. package/dist/shell/runtime.e50f6c3a5f7c74a0a717.js +0 -1
@@ -0,0 +1,90 @@
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 collectPageTypes from './collectPageTypes.js';
18
+
19
+ const page = {
20
+ id: 'page:today',
21
+ type: 'Screen',
22
+ slots: {
23
+ content: {
24
+ blocks: [
25
+ {
26
+ id: 'block:today:tap:0',
27
+ type: 'Button',
28
+ events: {
29
+ onClick: {
30
+ try: [{ id: 'go', type: 'Link', params: 'detail' }],
31
+ catch: [{ id: 'failed', type: 'SetState', params: {} }],
32
+ },
33
+ },
34
+ },
35
+ {
36
+ id: 'block:today:box:0',
37
+ type: 'Box',
38
+ slots: { content: { blocks: [{ id: 'block:today:text:0', type: 'Paragraph' }] } },
39
+ },
40
+ ],
41
+ },
42
+ },
43
+ };
44
+
45
+ const expected = {
46
+ blocks: ['Box', 'Button', 'Paragraph', 'Screen'],
47
+ actions: ['Link', 'SetState'],
48
+ };
49
+
50
+ test('collectPageTypes reads the block and action types of a page', () => {
51
+ expect(collectPageTypes(page)).toEqual(expected);
52
+ });
53
+
54
+ // The dev server sends page config serialized: an array that carries a config key comes as
55
+ // { '~arr': [...], '~k': key }.
56
+ test('collectPageTypes reads a serialized page as the dev server sends it', () => {
57
+ const serialized = {
58
+ id: 'page:today',
59
+ type: 'Screen',
60
+ slots: {
61
+ content: {
62
+ blocks: {
63
+ '~arr': [
64
+ {
65
+ id: 'block:today:tap:0',
66
+ type: 'Button',
67
+ events: {
68
+ onClick: {
69
+ try: { '~arr': [{ id: 'go', type: 'Link', params: 'detail' }], '~k': 'k2' },
70
+ catch: { '~arr': [{ id: 'failed', type: 'SetState', params: {} }], '~k': 'k3' },
71
+ },
72
+ },
73
+ },
74
+ {
75
+ id: 'block:today:box:0',
76
+ type: 'Box',
77
+ slots: {
78
+ content: {
79
+ blocks: { '~arr': [{ id: 'block:today:text:0', type: 'Paragraph' }], '~k': 'k4' },
80
+ },
81
+ },
82
+ },
83
+ ],
84
+ '~k': 'k1',
85
+ },
86
+ },
87
+ },
88
+ };
89
+ expect(collectPageTypes(serialized)).toEqual(expected);
90
+ });
@@ -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 checkTypeSet from './checkTypeSet.js';
18
+ import collectPageTypes from './collectPageTypes.js';
19
+
20
+ // The types a page needs that this shell lacks. A production root lists the page's type set under
21
+ // its typesKey; a dev root lists none, so the page's own config is read when it has arrived.
22
+ function findMissingPageTypes({ entry, manifest, page, root }) {
23
+ if (entry.typesKey !== undefined) {
24
+ return checkTypeSet({ manifest, typeSet: root.typeSets[entry.typesKey] ?? {} });
25
+ }
26
+ if (page === undefined) {
27
+ return [];
28
+ }
29
+ const used = collectPageTypes(page.pageConfig);
30
+ return [
31
+ ...used.blocks
32
+ .filter((name) => manifest.blocks[name] === undefined)
33
+ .map((name) => ({ category: 'blocks', type: name })),
34
+ ...used.actions
35
+ .filter((name) => manifest.actions[name] === undefined)
36
+ .map((name) => ({ category: 'actions', type: name })),
37
+ ];
38
+ }
39
+
40
+ export default findMissingPageTypes;
@@ -0,0 +1,38 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ function parseVersion(version) {
18
+ const [major = 0, minor = 0, patch = 0] = String(version)
19
+ .split('-')[0]
20
+ .split('.')
21
+ .map((part) => Number.parseInt(part, 10) || 0);
22
+ return [major, minor, patch];
23
+ }
24
+
25
+ // Compares two release versions ("7.4.0") by major, minor and patch: below 0 when a is older than
26
+ // b, 0 when they match, above 0 when a is newer. A prerelease suffix is ignored.
27
+ function compareVersions(a, b) {
28
+ const partsA = parseVersion(a);
29
+ const partsB = parseVersion(b);
30
+ for (let i = 0; i < 3; i += 1) {
31
+ if (partsA[i] !== partsB[i]) {
32
+ return partsA[i] - partsB[i];
33
+ }
34
+ }
35
+ return 0;
36
+ }
37
+
38
+ export default compareVersions;
@@ -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
+ import { type } from '@lowdefy/helpers';
18
+
19
+ import compareVersions from './compareVersions.js';
20
+
21
+ // mobile.minShellVersion blocks every page of an app on a shell older than it: it is for security
22
+ // fixes a server cannot work around.
23
+ function isShellTooOld({ minShellVersion, shellVersion }) {
24
+ if (type.isNone(minShellVersion)) {
25
+ return false;
26
+ }
27
+ return compareVersions(shellVersion, minShellVersion) < 0;
28
+ }
29
+
30
+ export default isShellTooOld;
@@ -0,0 +1,21 @@
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/createShellVersion.mjs from package.json. Do not edit.
18
+
19
+ const shellVersion = '7.2.0';
20
+
21
+ export default shellVersion;
@@ -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 { execFileSync } from 'child_process';
18
+ import path from 'path';
19
+
20
+ import shellPackage from '../../package.json';
21
+ import shellVersion from './shellVersion.js';
22
+
23
+ // The committed version the bundle carries must be the package's: run
24
+ // `pnpm --filter=@lowdefy/shell shell-version` after changing it. `pnpm release:version` does.
25
+ test('src/version/shellVersion.js matches the @lowdefy/shell package version', () => {
26
+ expect(shellVersion).toBe(shellPackage.version);
27
+ expect(
28
+ execFileSync(process.execPath, [path.join(__dirname, '../../scripts/createShellVersion.mjs')], {
29
+ encoding: 'utf8',
30
+ })
31
+ ).toBe(shellVersion);
32
+ });
@@ -0,0 +1,59 @@
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 callEndpoint from '../native/callEndpoint.js';
18
+ import reportShellError from '../host/reportShellError.js';
19
+
20
+ /**
21
+ * Each mobile.widgets source's data, from its endpoint: { [id]: { title, layout, url, count, label,
22
+ * items } }. A count source answers { count, label? }, a list source { items: [{ title, subtitle?
23
+ * }] }. A source whose endpoint fails keeps showing nothing new (it is left out, and the failure is
24
+ * reported). The url a widget
25
+ * opens is the source's path under the shell's scheme.
26
+ *
27
+ * @param {Object} params
28
+ * @param {Object} params.app The open app (currentApp.js).
29
+ */
30
+ async function fetchWidgetData({ app }) {
31
+ const sources = app.session.getState().root?.widgets ?? [];
32
+ const entries = await Promise.all(
33
+ sources.map(async (source) => {
34
+ try {
35
+ const data = await callEndpoint({ app, endpointId: source.endpointId });
36
+ return [
37
+ source.id,
38
+ {
39
+ title: source.title,
40
+ layout: source.layout,
41
+ url: `${app.shell.scheme}://${(source.path ?? '/').replace(/^\//, '')}`,
42
+ count: data?.count ?? null,
43
+ label: data?.label ?? null,
44
+ items: (data?.items ?? []).slice(0, 6).map((item) => ({
45
+ title: String(item.title ?? ''),
46
+ subtitle: item.subtitle === undefined ? null : String(item.subtitle),
47
+ })),
48
+ },
49
+ ];
50
+ } catch (error) {
51
+ reportShellError({ error, session: app.session });
52
+ return null;
53
+ }
54
+ })
55
+ );
56
+ return Object.fromEntries(entries.filter((entry) => entry !== null));
57
+ }
58
+
59
+ export default fetchWidgetData;
@@ -0,0 +1,156 @@
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 { Text, VStack, HStack, Spacer } from '@expo/ui/swift-ui';
18
+ import { font, foregroundStyle, lineLimit, widgetURL } from '@expo/ui/swift-ui/modifiers';
19
+ import { createWidget } from 'expo-widgets';
20
+
21
+ import fetchWidgetData from './fetchWidgetData.js';
22
+ import { getCurrentApp } from '../native/currentApp.js';
23
+
24
+ // The two fixed layouts, drawn by the widget extension from the props the app hands them
25
+ // ({ sources }) and the source the person picked when adding the widget (its `source` parameter).
26
+ // They run in the widget's own JavaScript runtime ('widget' directive), so they read nothing but
27
+ // their arguments.
28
+ function CountLayout(props, environment) {
29
+ 'widget';
30
+ const sources = props.sources ?? {};
31
+ const source = sources[environment.configuration?.source] ?? sources[Object.keys(sources)[0]];
32
+ if (!source) {
33
+ return <Text>Open the app to load this widget.</Text>;
34
+ }
35
+ if (environment.widgetFamily === 'accessoryInline') {
36
+ return (
37
+ <Text modifiers={[widgetURL(source.url)]}>{`${source.count ?? '–'} ${
38
+ source.label ?? source.title
39
+ }`}</Text>
40
+ );
41
+ }
42
+ return (
43
+ <VStack alignment="leading" modifiers={[widgetURL(source.url)]}>
44
+ <Text
45
+ modifiers={[
46
+ font({ size: 13, weight: 'semibold' }),
47
+ foregroundStyle({ type: 'hierarchical', style: 'secondary' }),
48
+ ]}
49
+ >
50
+ {source.title}
51
+ </Text>
52
+ <Spacer />
53
+ <Text modifiers={[font({ size: 40, weight: 'bold' })]}>{String(source.count ?? '–')}</Text>
54
+ {source.label ? (
55
+ <Text
56
+ modifiers={[
57
+ font({ size: 13 }),
58
+ foregroundStyle({ type: 'hierarchical', style: 'secondary' }),
59
+ lineLimit(1),
60
+ ]}
61
+ >
62
+ {source.label}
63
+ </Text>
64
+ ) : null}
65
+ </VStack>
66
+ );
67
+ }
68
+
69
+ function ListLayout(props, environment) {
70
+ 'widget';
71
+ const sources = props.sources ?? {};
72
+ const source = sources[environment.configuration?.source] ?? sources[Object.keys(sources)[0]];
73
+ if (!source) {
74
+ return <Text>Open the app to load this widget.</Text>;
75
+ }
76
+ const rows = environment.widgetFamily === 'systemLarge' ? 6 : 3;
77
+ return (
78
+ <VStack alignment="leading" spacing={6} modifiers={[widgetURL(source.url)]}>
79
+ <Text
80
+ modifiers={[
81
+ font({ size: 13, weight: 'semibold' }),
82
+ foregroundStyle({ type: 'hierarchical', style: 'secondary' }),
83
+ ]}
84
+ >
85
+ {source.title}
86
+ </Text>
87
+ {source.items.slice(0, rows).map((item, index) => (
88
+ <HStack key={index}>
89
+ <VStack alignment="leading" spacing={0}>
90
+ <Text modifiers={[font({ size: 15, weight: 'medium' }), lineLimit(1)]}>
91
+ {item.title}
92
+ </Text>
93
+ {item.subtitle ? (
94
+ <Text
95
+ modifiers={[
96
+ font({ size: 12 }),
97
+ foregroundStyle({ type: 'hierarchical', style: 'secondary' }),
98
+ lineLimit(1),
99
+ ]}
100
+ >
101
+ {item.subtitle}
102
+ </Text>
103
+ ) : null}
104
+ </VStack>
105
+ <Spacer />
106
+ </HStack>
107
+ ))}
108
+ {source.items.length === 0 ? (
109
+ <Text modifiers={[foregroundStyle({ type: 'hierarchical', style: 'secondary' })]}>
110
+ Nothing here.
111
+ </Text>
112
+ ) : null}
113
+ <Spacer />
114
+ </VStack>
115
+ );
116
+ }
117
+
118
+ const countWidget = createWidget('LowdefyCount', CountLayout, { sources: {} });
119
+ const listWidget = createWidget('LowdefyList', ListLayout, { sources: {} });
120
+
121
+ // The sources a person picks from when adding a widget, by layout.
122
+ function setSourceOptions(sources) {
123
+ [
124
+ [countWidget, 'count'],
125
+ [listWidget, 'list'],
126
+ ].forEach(([widget, layout]) => {
127
+ widget.setConfigurationParameterEnum(
128
+ 'source',
129
+ Object.entries(sources)
130
+ .filter(([, source]) => source.layout === layout)
131
+ .map(([value, source]) => ({ name: source.title, value }))
132
+ );
133
+ });
134
+ }
135
+
136
+ /**
137
+ * The widgets device feature (the widgets extension): RefreshWidgets, and the shell's own refreshes
138
+ * (refreshWidgets.js), fetch every source and hand each layout the sources of its kind.
139
+ */
140
+ const widgets = {
141
+ async refresh() {
142
+ const sources = await fetchWidgetData({ app: await getCurrentApp() });
143
+ setSourceOptions(sources);
144
+ const pick = (layout) =>
145
+ Object.fromEntries(Object.entries(sources).filter(([, source]) => source.layout === layout));
146
+ countWidget.updateSnapshot({ sources: pick('count') });
147
+ listWidget.updateSnapshot({ sources: pick('list') });
148
+ return { sources: Object.keys(sources).length };
149
+ },
150
+ };
151
+
152
+ // The widgets extension's device features (profiles.js WIDGETS_ENTRY).
153
+ const features = { widgets };
154
+ const hosts = [];
155
+
156
+ export { features, hosts };
@@ -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 { getDeviceFeature, hasDeviceFeature } from '@lowdefy/blocks-native/device';
18
+
19
+ /**
20
+ * Refreshes the Home Screen and Lock Screen widgets from the app's mobile.widgets endpoints: at
21
+ * launch, on resume, on a push with data.refreshWidgets, on background fetch and on RefreshWidgets.
22
+ * A shell without the widgets extension has nothing to refresh.
23
+ */
24
+ async function refreshWidgets() {
25
+ if (!hasDeviceFeature('widgets')) {
26
+ return null;
27
+ }
28
+ return getDeviceFeature({ feature: 'widgets', action: 'RefreshWidgets' }).refresh();
29
+ }
30
+
31
+ export default refreshWidgets;
@@ -0,0 +1,93 @@
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 { withPodfile, withRunOnce, withSettingsGradle } = require('expo/config-plugins');
18
+
19
+ function quotedList(names) {
20
+ return `[${names.map((name) => `'${name}'`).join(', ')}]`;
21
+ }
22
+
23
+ // Replaces `pattern` once in a native project file, and fails the prebuild when the Expo template no
24
+ // longer has it: a shell that links a module it should not must never build quietly.
25
+ function replaceOnce({ contents, file, pattern, replace }) {
26
+ if (!pattern.test(contents)) {
27
+ throw new Error(
28
+ `Cannot exclude native modules: ${file} no longer matches ${pattern}. Update withExcludedNativeModules.js for this Expo template.`
29
+ );
30
+ }
31
+ return contents.replace(pattern, replace);
32
+ }
33
+
34
+ /**
35
+ * Leaves native modules out of a shell's native projects (getExclusions in shellVariant.js), so a
36
+ * shell carries only its capability profile's modules, and a release shell none of Lowdefy Dev's:
37
+ * - iOS: `use_expo_modules!` and the react-native-config command in the Podfile get `exclude`.
38
+ * - Android: settings.gradle sets `expoAutolinking.exclude` and adds `--exclude` to the
39
+ * react-native-config command.
40
+ * - Each module's config plugin is marked as run, so prebuild does not apply it on its own and
41
+ * add its permissions (NSCameraUsageDescription, NSMotionUsageDescription, CAMERA).
42
+ *
43
+ * @param {Object} config The Expo config.
44
+ * @param {string[]} modules The package names to leave out.
45
+ */
46
+ function withExcludedNativeModules(config, modules) {
47
+ if (modules.length === 0) {
48
+ return config;
49
+ }
50
+ let result = modules.reduce(
51
+ (current, name) => withRunOnce(current, { plugin: (unchanged) => unchanged, name }),
52
+ config
53
+ );
54
+ result = withPodfile(result, (podfileConfig) => {
55
+ let { contents } = podfileConfig.modResults;
56
+ contents = replaceOnce({
57
+ contents,
58
+ file: 'Podfile',
59
+ pattern: /^(\s*)use_expo_modules!\s*$/m,
60
+ replace: `$1use_expo_modules!(exclude: ${quotedList(modules)})`,
61
+ });
62
+ contents = replaceOnce({
63
+ contents,
64
+ file: 'Podfile',
65
+ pattern: /('react-native-config',\s*'--json',\s*'--platform',\s*'ios')/,
66
+ replace: `$1,\n ${modules.map((name) => `'--exclude', '${name}'`).join(',\n ')}`,
67
+ });
68
+ podfileConfig.modResults.contents = contents;
69
+ return podfileConfig;
70
+ });
71
+ result = withSettingsGradle(result, (settingsConfig) => {
72
+ let { contents } = settingsConfig.modResults;
73
+ contents = replaceOnce({
74
+ contents,
75
+ file: 'settings.gradle',
76
+ pattern: /^expoAutolinking\.useExpoModules\(\)\s*$/m,
77
+ replace: `expoAutolinking.exclude = ${quotedList(modules)}\nexpoAutolinking.useExpoModules()`,
78
+ });
79
+ contents = replaceOnce({
80
+ contents,
81
+ file: 'settings.gradle',
82
+ pattern: /ex\.autolinkLibrariesFromCommand\(expoAutolinking\.rnConfigCommand\)/,
83
+ replace: `ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand + ${quotedList(
84
+ modules.flatMap((name) => ['--exclude', name])
85
+ )})`,
86
+ });
87
+ settingsConfig.modResults.contents = contents;
88
+ return settingsConfig;
89
+ });
90
+ return result;
91
+ }
92
+
93
+ module.exports = withExcludedNativeModules;
package/dist/location.js DELETED
@@ -1,33 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.shellDirectory = exports.publicDirectory = void 0;
7
-
8
- var _path = _interopRequireDefault(require("path"));
9
-
10
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
-
12
- /*
13
- Copyright 2020-2021 Lowdefy, Inc
14
-
15
- Licensed under the Apache License, Version 2.0 (the "License");
16
- you may not use this file except in compliance with the License.
17
- You may obtain a copy of the License at
18
-
19
- http://www.apache.org/licenses/LICENSE-2.0
20
-
21
- Unless required by applicable law or agreed to in writing, software
22
- distributed under the License is distributed on an "AS IS" BASIS,
23
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
- See the License for the specific language governing permissions and
25
- limitations under the License.
26
- */
27
- var publicDirectory = _path.default.resolve(__dirname, 'public');
28
-
29
- exports.publicDirectory = publicDirectory;
30
-
31
- var shellDirectory = _path.default.resolve(__dirname, 'shell');
32
-
33
- exports.shellDirectory = shellDirectory;
Binary file
Binary file
Binary file
@@ -1,17 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
- <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
- <svg width="100%" height="100%" viewBox="0 0 94 94" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
4
- <g transform="matrix(1,0,0,1,-979.672,-59.6924)">
5
- <g transform="matrix(1,0,0,1.03297,-38.3284,-294.615)">
6
- <g transform="matrix(1,0,0,1,952,232)">
7
- <path d="M160,129.634C160,119.35 151.375,111 140.751,111L85.249,111C74.625,111 66,119.35 66,129.634L66,183.366C66,193.65 74.625,202 85.249,202L140.751,202C151.375,202 160,193.65 160,183.366L160,129.634Z"/>
8
- </g>
9
- <g transform="matrix(0.872141,0,0,1,1002.6,346)">
10
- <rect x="36" y="12" width="36" height="59" style="fill:white;"/>
11
- </g>
12
- <g transform="matrix(0.78125,0,0,0.862069,1010.84,356.663)">
13
- <rect x="77" y="41" width="32" height="29" style="fill:rgb(24,144,255);"/>
14
- </g>
15
- </g>
16
- </g>
17
- </svg>
Binary file
Binary file