From 5bf095178d3248ae745118fc34f5dc565b55a502 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Fri, 14 Nov 2025 18:12:29 +0100 Subject: [PATCH] Add live updates for the webview app --- android/README.md | 36 +++++++++++++++++++++++++----- android/app/build.gradle | 4 ++-- android/app/capacitor.build.gradle | 1 + android/capacitor.settings.gradle | 3 +++ capacitor.config.ts | 7 +++++- package.json | 1 + web/pages/_app.tsx | 31 +++++++++++++++++-------- yarn.lock | 5 +++++ 8 files changed, 70 insertions(+), 18 deletions(-) diff --git a/android/README.md b/android/README.md index 72e9f9ce..74523e00 100644 --- a/android/README.md +++ b/android/README.md @@ -257,17 +257,41 @@ npx cap sync android ## 14. Deployment Workflow ```bash -# 1. Build web app for production -yarn build-web +# Build web app for production and Sync assets to Android +yarn build-sync-android -# 2. Sync assets to Android -npx cap sync android - -# 3. Build signed release APK in Android Studio +# Build signed release APK in Android Studio ``` --- +## Live Updates + +To avoid releasing to the app stores after every code update in the web pages, we build the new bundle and store it in Capawesome Cloud (an alternative to Ionic). + +First, you need to do this one-time setup: +``` +npm install -g @capawesome/cli@latest +npx @capawesome/cli login +``` + +Then, run this to build your local assets and push them to Capawesome. Once done, each mobile app user will receive a notice that there is a new update available, which they can approve to download. +``` +yarn build-web +npx @capawesome/cli apps:bundles:create --path web/out +``` + +That's all. So you should run the lines above every time you want your web updates pushed to main (which essentially updates the web app) to update the mobile app as well. +Maybe we should add it to our CD. For example we set a file with `{liveUpdateVersion: 1}` and run the live update each time a push to main increments that counter. +There is a limit of 100 monthly active user per month, though. So we may need to pay or create our custom limit as we scale. Next plan is $9 / month and allows 1000 MAUs. + +- ∞ Live Updates +- 100 Monthly Active Users +- 500 MB of Storage (around 10 MB per update, but we just delete the previous ones) +- 5 GB of Bandwidth + +--- + ## 15. Resources * [Capacitor Docs](https://capacitorjs.com/docs) diff --git a/android/app/build.gradle b/android/app/build.gradle index 8b476933..e8716308 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "com.compassconnections.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 13 - versionName "1.1.2" + versionCode 14 + versionName "1.1.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 0947d250..f4e3fa4c 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -13,6 +13,7 @@ dependencies { implementation project(':capacitor-keyboard') implementation project(':capacitor-push-notifications') implementation project(':capacitor-status-bar') + implementation project(':capawesome-capacitor-live-update') implementation project(':capgo-capacitor-social-login') } diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 99f4debc..1cf75a4a 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -14,5 +14,8 @@ project(':capacitor-push-notifications').projectDir = new File('../node_modules/ include ':capacitor-status-bar' project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android') +include ':capawesome-capacitor-live-update' +project(':capawesome-capacitor-live-update').projectDir = new File('../node_modules/@capawesome/capacitor-live-update/android') + include ':capgo-capacitor-social-login' project(':capgo-capacitor-social-login').projectDir = new File('../node_modules/@capgo/capacitor-social-login/android') diff --git a/capacitor.config.ts b/capacitor.config.ts index fa7fac66..fddd19a6 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -9,7 +9,12 @@ const config: CapacitorConfig = { appId: 'com.compassconnections.app', appName: 'Compass', webDir: 'web/out', - server: LOCAL_ANDROID ? { url: `http://${LOCAL_URL}:3000`, cleartext: true } : {} + server: LOCAL_ANDROID ? { url: `http://${LOCAL_URL}:3000`, cleartext: true } : {}, + plugins: { + LiveUpdate: { + appId: "969bc540-8077-492f-8403-b554bee5de50" + } + } }; export default config; diff --git a/package.json b/package.json index 456d2533..4cff2de9 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@capacitor/keyboard": "7.0.3", "@capacitor/push-notifications": "7.0.3", "@capacitor/status-bar": "7.0.3", + "@capawesome/capacitor-live-update": "7.2.2", "@capgo/capacitor-social-login": "7.14.9", "@playwright/test": "^1.54.2", "colorette": "^2.0.20", diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index d9b81839..8c3b681d 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -16,17 +16,30 @@ import {isAndroidWebView} from "web/lib/util/webview" import {Capacitor} from '@capacitor/core' import {StatusBar} from '@capacitor/status-bar' import {App} from '@capacitor/app' -import {useRouter} from "next/navigation"; -import {Keyboard} from "@capacitor/keyboard"; +import {useRouter} from "next/navigation" +import {Keyboard} from "@capacitor/keyboard" +import {LiveUpdate} from "@capawesome/capacitor-live-update" if (Capacitor.isNativePlatform()) { // Only runs on iOS/Android native // Note sure it's doing anything, though, need to check StatusBar.setOverlaysWebView({overlay: false}).catch(console.warn) // StatusBar.setStyle({style: Style.Light}).catch(console.warn) + App.addListener('backButton', () => { window.dispatchEvent(new CustomEvent('appBackButton')) }) + + App.addListener("resume", async () => { + const {nextBundleId} = await LiveUpdate.sync() + if (nextBundleId) { + // Ask the user if they want to apply the update immediately + const shouldReload = confirm("A new update is available. Would you like to install it?") + if (shouldReload) { + await LiveUpdate.reload() + } + } + }) } @@ -73,16 +86,16 @@ function MyApp({Component, pageProps}: AppProps) { useEffect(() => { console.log('isAndroidWebView app:', isAndroidWebView()) if (!Capacitor.isNativePlatform()) return - const onShow = () => document.body.classList.add('keyboard-open'); - const onHide = () => document.body.classList.remove('keyboard-open'); + const onShow = () => document.body.classList.add('keyboard-open') + const onHide = () => document.body.classList.remove('keyboard-open') - Keyboard.addListener('keyboardWillShow', onShow); - Keyboard.addListener('keyboardWillHide', onHide); + Keyboard.addListener('keyboardWillShow', onShow) + Keyboard.addListener('keyboardWillHide', onHide) return () => { - Keyboard.removeAllListeners(); - }; - }, []); + Keyboard.removeAllListeners() + } + }, []) useEffect(() => { initTracking() diff --git a/yarn.lock b/yarn.lock index b091bc64..8be4cfe5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1207,6 +1207,11 @@ resolved "https://registry.yarnpkg.com/@capacitor/status-bar/-/status-bar-7.0.3.tgz#4e8c1ac49cf928576cfac1c78cc3f9886088ebfe" integrity sha512-JyRpVnKwHij9hgPWolF6PK+HT3e2HSPjN11/h2OmKxq8GAdPGARFLv+97eZl0pvuvm0Kka/LpiLb5whXISBg7Q== +"@capawesome/capacitor-live-update@7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@capawesome/capacitor-live-update/-/capacitor-live-update-7.2.2.tgz#404581ef8a22329b6f61d36cb8a166ea8c8f2f82" + integrity sha512-38fltILlpVFofY+Bz9yYXz/fl/zmKxa5F/goXmmpY+DfkfJvhFbWj92yqvRI2GhlDKbOxNyCoc9p8zYztYM4dQ== + "@capgo/capacitor-social-login@7.14.9": version "7.14.9" resolved "https://registry.yarnpkg.com/@capgo/capacitor-social-login/-/capacitor-social-login-7.14.9.tgz#19a79605dfbfbfe2afb9b6a082affa20460016f5"