From fdbb650baa0023446534ff0966c22967eb179c21 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Thu, 30 Apr 2026 18:58:40 +0700 Subject: [PATCH] build(android): add fdroid distribution flavor --- .github/workflows/ci.yml | 4 +- .github/workflows/release.yml | 18 +++--- android/app/build.gradle | 38 +++++++++++- .../java/fivechan/android/MainActivity.java | 15 +++++ android/app/src/github/AndroidManifest.xml | 4 ++ .../fivechan/android/AppUpdaterPlugin.java | 0 .../java/fivechan/android/MainActivity.java | 2 +- android/app/src/main/AndroidManifest.xml | 1 - .../android/AppUpdaterPluginTest.java | 0 docs/fdroid/fivechan.android.yml | 46 ++++++++++++++ .../metadata/android/en-US/changelogs/802.txt | 1 + .../android/en-US/full_description.txt | 5 ++ .../android/en-US/short_description.txt | 1 + fastlane/metadata/android/en-US/title.txt | 1 + package.json | 3 +- .../__tests__/interface-settings.test.tsx | 22 +++++-- .../interface-settings/app-update-setting.tsx | 61 +++++++++++++++++++ .../interface-settings/interface-settings.tsx | 57 +++-------------- src/env.d.ts | 2 + src/index.tsx | 8 ++- src/lib/__tests__/app-update.test.ts | 22 +++++++ src/lib/app-distribution.ts | 4 ++ src/lib/app-update.ts | 13 +++- src/lib/app-version.ts | 4 +- vite.config.js | 2 + 25 files changed, 259 insertions(+), 75 deletions(-) create mode 100644 android/app/src/fdroid/java/fivechan/android/MainActivity.java create mode 100644 android/app/src/github/AndroidManifest.xml rename android/app/src/{main => github}/java/fivechan/android/AppUpdaterPlugin.java (100%) rename android/app/src/{main => github}/java/fivechan/android/MainActivity.java (100%) rename android/app/src/{test => testGithub}/java/fivechan/android/AppUpdaterPluginTest.java (100%) create mode 100644 docs/fdroid/fivechan.android.yml create mode 100644 fastlane/metadata/android/en-US/changelogs/802.txt create mode 100644 fastlane/metadata/android/en-US/full_description.txt create mode 100644 fastlane/metadata/android/en-US/short_description.txt create mode 100644 fastlane/metadata/android/en-US/title.txt create mode 100644 src/components/settings-modal/interface-settings/app-update-setting.tsx create mode 100644 src/lib/app-distribution.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index caad3864..ff99e35b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -175,8 +175,8 @@ jobs: - name: Sync Capacitor Android project run: npx cap sync android - - name: Run Android unit tests and assemble debug APK - run: cd android && ./gradlew testDebugUnitTest assembleDebug --stacktrace + - name: Run Android unit tests and assemble debug APKs + run: cd android && ./gradlew testGithubDebugUnitTest testFdroidDebugUnitTest assembleGithubDebug assembleFdroidDebug --stacktrace package-linux: name: Package Linux (Ubuntu) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa35f451..d3dd233b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -263,28 +263,30 @@ jobs: run: CI='' NODE_ENV=production yarn build - name: Set Android versionCode and versionName run: | - sed -i "s/versionCode 1/versionCode $(git tag | wc -l)/" ./android/app/build.gradle - sed -i "s/versionName \"1.0\"/versionName \"$(node -e "console.log(require('./package.json').version)")\"/" ./android/app/build.gradle - cat ./android/app/build.gradle + VERSION=$(node -e "console.log(require('./package.json').version)") + VERSION_CODE=$(node -e "const [major = 0, minor = 0, patch = 0] = require('./package.json').version.replace(/^v/, '').split('-')[0].split('.').map(Number); console.log((major * 10000) + (minor * 100) + patch)") + echo "APP_VERSION_NAME=${VERSION}" >> "$GITHUB_ENV" + echo "APP_VERSION_CODE=${VERSION_CODE}" >> "$GITHUB_ENV" + echo "Android versionName=${VERSION} versionCode=${VERSION_CODE}" - name: Sync Capacitor run: npx cap sync android - name: Build APK run: | for i in 1 2 3; do - echo "gradlew assembleRelease attempt $i" - (cd android && ./gradlew assembleRelease --stacktrace) && break + echo "gradlew assembleGithubRelease attempt $i" + (cd android && ./gradlew assembleGithubRelease -PAPP_VERSION_NAME="${APP_VERSION_NAME}" -PAPP_VERSION_CODE="${APP_VERSION_CODE}" --stacktrace) && break sleep 10 [ "$i" = "3" ] && exit 1 done - name: Optimize APK - run: cd android/app/build/outputs/apk/release && zipalign 4 app-release-unsigned.apk app-release-unsigned-zip.apk + run: cd android/app/build/outputs/apk/github/release && zipalign 4 app-github-release-unsigned.apk app-github-release-unsigned-zip.apk - name: Sign APK - run: cd android/app/build/outputs/apk/release && apksigner sign --ks ../../../../../plebbit.keystore --ks-pass pass:${{ secrets.PLEBBIT_REACT_KEYSTORE_PASSWORD }} --ks-key-alias release --out app-release-signed.apk app-release-unsigned-zip.apk + run: cd android/app/build/outputs/apk/github/release && apksigner sign --ks ../../../../../../plebbit.keystore --ks-pass pass:${{ secrets.PLEBBIT_REACT_KEYSTORE_PASSWORD }} --ks-key-alias release --out app-github-release-signed.apk app-github-release-unsigned-zip.apk - name: Stage release artifacts run: | mkdir -p release-assets VERSION=$(node -e "console.log(require('./package.json').version)") - mv android/app/build/outputs/apk/release/app-release-signed.apk "release-assets/5chan-${VERSION}.apk" + mv android/app/build/outputs/apk/github/release/app-github-release-signed.apk "release-assets/5chan-${VERSION}.apk" - name: List release assets run: ls -la release-assets - uses: actions/upload-artifact@v4 diff --git a/android/app/build.gradle b/android/app/build.gradle index bd93a4cb..59284cba 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,30 @@ apply plugin: 'com.android.application' +import groovy.json.JsonSlurper + +def resolvePackageVersionName() { + def packageJsonFile = file("../../package.json") + if (!packageJsonFile.exists()) { + return "1.0" + } + + return new JsonSlurper().parse(packageJsonFile).version.toString() +} + +def versionNameToCode(String versionName) { + def stableVersion = versionName.replaceFirst(/^v/, "").split("-")[0] + def parts = stableVersion.tokenize(".").collect { part -> part.isInteger() ? part.toInteger() : 0 } + + while (parts.size() < 3) { + parts.add(0) + } + + return (parts[0] * 10000) + (parts[1] * 100) + parts[2] +} + +def resolvedVersionName = "${project.findProperty("APP_VERSION_NAME") ?: resolvePackageVersionName()}" +def resolvedVersionCode = Integer.parseInt("${project.findProperty("APP_VERSION_CODE") ?: versionNameToCode(resolvedVersionName)}") + android { namespace "fivechan.android" compileSdkVersion rootProject.ext.compileSdkVersion @@ -7,8 +32,8 @@ android { applicationId "fivechan.android" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode Integer.parseInt(project.findProperty("APP_VERSION_CODE") ?: "1") - versionName "${project.findProperty("APP_VERSION_NAME") ?: "1.0"}" + versionCode resolvedVersionCode + versionName resolvedVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. @@ -16,6 +41,15 @@ android { ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' } } + flavorDimensions "distribution" + productFlavors { + github { + dimension "distribution" + } + fdroid { + dimension "distribution" + } + } buildTypes { release { minifyEnabled false diff --git a/android/app/src/fdroid/java/fivechan/android/MainActivity.java b/android/app/src/fdroid/java/fivechan/android/MainActivity.java new file mode 100644 index 00000000..8ff5be10 --- /dev/null +++ b/android/app/src/fdroid/java/fivechan/android/MainActivity.java @@ -0,0 +1,15 @@ +package fivechan.android; + +import android.os.Bundle; +import com.capacitorjs.plugins.statusbar.StatusBarPlugin; +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + // Custom plugins must be registered before bridge initialization. + registerPlugin(FileUploaderPlugin.class); + registerPlugin(StatusBarPlugin.class); + super.onCreate(savedInstanceState); + } +} diff --git a/android/app/src/github/AndroidManifest.xml b/android/app/src/github/AndroidManifest.xml new file mode 100644 index 00000000..aa8c63f9 --- /dev/null +++ b/android/app/src/github/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/android/app/src/main/java/fivechan/android/AppUpdaterPlugin.java b/android/app/src/github/java/fivechan/android/AppUpdaterPlugin.java similarity index 100% rename from android/app/src/main/java/fivechan/android/AppUpdaterPlugin.java rename to android/app/src/github/java/fivechan/android/AppUpdaterPlugin.java diff --git a/android/app/src/main/java/fivechan/android/MainActivity.java b/android/app/src/github/java/fivechan/android/MainActivity.java similarity index 100% rename from android/app/src/main/java/fivechan/android/MainActivity.java rename to android/app/src/github/java/fivechan/android/MainActivity.java index a80cf896..ad618f7f 100644 --- a/android/app/src/main/java/fivechan/android/MainActivity.java +++ b/android/app/src/github/java/fivechan/android/MainActivity.java @@ -1,8 +1,8 @@ package fivechan.android; import android.os.Bundle; -import com.getcapacitor.BridgeActivity; import com.capacitorjs.plugins.statusbar.StatusBarPlugin; +import com.getcapacitor.BridgeActivity; public class MainActivity extends BridgeActivity { @Override diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index dcf35e35..7c770b88 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,6 @@ - { }); }; +const settleLazyImports = async () => { + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); +}; + const findButtonByText = (text: string) => Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === text); describe('InterfaceSettings', () => { @@ -180,25 +187,27 @@ describe('InterfaceSettings', () => { expect(localStorage.getItem(INTERFACE_LANGUAGE_STORAGE_KEY)).toBe('fr'); }); - it('renders a check button when no app update is available', () => { + it('renders a check button when no app update is available', async () => { render(createElement(InterfaceSettings)); + await settleLazyImports(); expect(container.textContent).toContain('Update:'); expect(findButtonByText('Check')).toBeTruthy(); }); - it('shows the checking status while an update check is in progress', () => { + it('shows the checking status while an update check is in progress', async () => { useAppUpdateStore.setState({ isCheckingForUpdate: true, }); render(createElement(InterfaceSettings)); + await settleLazyImports(); expect(findButtonByText('Check')?.disabled).toBe(true); expect(container.textContent).toContain('checking_for_updates'); }); - it('renders a download button and release link when an app update is available', () => { + it('renders a download button and release link when an app update is available', async () => { useAppUpdateStore.setState({ availableUpdate: { runtime: 'web', @@ -208,6 +217,7 @@ describe('InterfaceSettings', () => { }); render(createElement(InterfaceSettings)); + await settleLazyImports(); expect(findButtonByText('Download')).toBeTruthy(); const releaseLink = container.querySelector('a[href="https://github.com/bitsocialnet/5chan/releases/tag/v9.9.9"]'); @@ -217,6 +227,7 @@ describe('InterfaceSettings', () => { it('checks for app updates when the check button is pressed', async () => { render(createElement(InterfaceSettings)); + await settleLazyImports(); const button = findButtonByText('Check'); expect(button).toBeTruthy(); @@ -242,6 +253,7 @@ describe('InterfaceSettings', () => { }); render(createElement(InterfaceSettings)); + await settleLazyImports(); const button = findButtonByText('Download'); expect(button).toBeTruthy(); @@ -255,7 +267,7 @@ describe('InterfaceSettings', () => { expect(testState.alertMock).not.toHaveBeenCalled(); }); - it('disables the update button while an app update is already being applied', () => { + it('disables the update button while an app update is already being applied', async () => { useAppUpdateStore.setState({ availableUpdate: { runtime: 'web', @@ -266,6 +278,7 @@ describe('InterfaceSettings', () => { }); render(createElement(InterfaceSettings)); + await settleLazyImports(); expect(findButtonByText('Download')?.disabled).toBe(true); }); @@ -283,6 +296,7 @@ describe('InterfaceSettings', () => { }); render(createElement(InterfaceSettings)); + await settleLazyImports(); const button = findButtonByText('Download'); expect(button).toBeTruthy(); diff --git a/src/components/settings-modal/interface-settings/app-update-setting.tsx b/src/components/settings-modal/interface-settings/app-update-setting.tsx new file mode 100644 index 00000000..3bef0c95 --- /dev/null +++ b/src/components/settings-modal/interface-settings/app-update-setting.tsx @@ -0,0 +1,61 @@ +import { useTranslation } from 'react-i18next'; +import capitalize from 'lodash/capitalize'; +import styles from './interface-settings.module.css'; +import useAppUpdateStore from '../../../stores/use-app-update-store'; + +const UpdateButton = () => { + const { t } = useTranslation(); + const availableUpdate = useAppUpdateStore((state) => state.availableUpdate); + const isApplyingUpdate = useAppUpdateStore((state) => state.isApplyingUpdate); + const isCheckingForUpdate = useAppUpdateStore((state) => state.isCheckingForUpdate); + const applyAppUpdate = useAppUpdateStore((state) => state.applyAppUpdate); + const refreshAvailableUpdate = useAppUpdateStore((state) => state.refreshAvailableUpdate); + + const handleUpdateAction = async () => { + try { + if (availableUpdate) { + await applyAppUpdate(); + return; + } + + await refreshAvailableUpdate(); + } catch (error) { + alert(String(error)); + } + }; + const buttonLabel = availableUpdate ? t('download') : t('check'); + const isBusy = isApplyingUpdate || isCheckingForUpdate; + + return ( + <> + + {isCheckingForUpdate && ( + + {t('checking_for_updates')} + + )} + {!isCheckingForUpdate && availableUpdate && ( + + {t('new_version_found')}:  + + v{availableUpdate.targetVersion} + + + )} + + ); +}; + +const AppUpdateSetting = () => { + const { t } = useTranslation(); + + return ( +
+ {capitalize(t('update'))}: +
+ ); +}; + +export default AppUpdateSetting; diff --git a/src/components/settings-modal/interface-settings/interface-settings.tsx b/src/components/settings-modal/interface-settings/interface-settings.tsx index df9e30ff..915ec830 100644 --- a/src/components/settings-modal/interface-settings/interface-settings.tsx +++ b/src/components/settings-modal/interface-settings/interface-settings.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { lazy, memo, Suspense } from 'react'; import { useTranslation } from 'react-i18next'; import styles from './interface-settings.module.css'; import capitalize from 'lodash/capitalize'; @@ -7,52 +7,9 @@ import useFeedViewSettingsStore from '../../../stores/use-feed-view-settings-sto import Version from '../../version'; import StyleSelector from '../../style-selector/style-selector'; import { INTERFACE_LANGUAGE_STORAGE_KEY, SUPPORTED_INTERFACE_LANGUAGES } from '../../../lib/constants'; -import useAppUpdateStore from '../../../stores/use-app-update-store'; -const UpdateButton = () => { - const { t } = useTranslation(); - const availableUpdate = useAppUpdateStore((state) => state.availableUpdate); - const isApplyingUpdate = useAppUpdateStore((state) => state.isApplyingUpdate); - const isCheckingForUpdate = useAppUpdateStore((state) => state.isCheckingForUpdate); - const applyAppUpdate = useAppUpdateStore((state) => state.applyAppUpdate); - const refreshAvailableUpdate = useAppUpdateStore((state) => state.refreshAvailableUpdate); - - const handleUpdateAction = async () => { - try { - if (availableUpdate) { - await applyAppUpdate(); - return; - } - - await refreshAvailableUpdate(); - } catch (error) { - alert(String(error)); - } - }; - const buttonLabel = availableUpdate ? t('download') : t('check'); - const isBusy = isApplyingUpdate || isCheckingForUpdate; - - return ( - <> - - {isCheckingForUpdate && ( - - {t('checking_for_updates')} - - )} - {!isCheckingForUpdate && availableUpdate && ( - - {t('new_version_found')}:  - - v{availableUpdate.targetVersion} - - - )} - - ); -}; +const shouldRenderAppUpdateSetting = import.meta.env.VITE_APP_DISTRIBUTION !== 'fdroid'; +const AppUpdateSetting = shouldRenderAppUpdateSetting ? lazy(() => import('./app-update-setting')) : null; const InterfaceLanguage = () => { const { i18n } = useTranslation(); @@ -87,9 +44,11 @@ const InterfaceSettings = () => {
{capitalize(t('version'))}:
-
- {capitalize(t('update'))}: -
+ {AppUpdateSetting && ( + + + + )}
{capitalize(t('interface_language'))}:
diff --git a/src/env.d.ts b/src/env.d.ts index 84d2f5b5..93620ad0 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -3,6 +3,8 @@ declare interface ImportMetaEnv { readonly VITE_COMMIT_REF: string; + readonly VITE_APP_VERSION?: string; + readonly VITE_APP_DISTRIBUTION?: string; } declare interface ImportMeta { diff --git a/src/index.tsx b/src/index.tsx index ed375379..09a36e30 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,13 +8,13 @@ import './index.css'; import './themes.css'; import AppUpdateRegistration from './components/app-update-registration'; import { App as CapacitorApp } from '@capacitor/app'; -import { Analytics } from '@vercel/analytics/react'; import { configureP2PBrowserPkcOptions } from './lib/p2p-browser-config'; // Only enable analytics on 5chan.app (Vercel deployment) // Exclude Electron (file:// or localhost), Capacitor/APK (capacitor:// or localhost), and IPFS (ipfs:// or different domain) const isVercelDeployment = typeof window !== 'undefined' && (window.location.hostname === '5chan.app' || window.location.hostname === 'www.5chan.app') && !window.isElectron; +const shouldLoadAnalytics = import.meta.env.VITE_APP_DISTRIBUTION !== 'fdroid' && isVercelDeployment; const e2eStartHash = import.meta.env.VITE_E2E_START_HASH?.trim(); const requestedE2EHarness = import.meta.env.DEV && typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('e2e') : null; @@ -27,6 +27,7 @@ configureP2PBrowserPkcOptions(); const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); const renderRoot = async () => { let e2eHarness: React.ComponentType | null = null; + let Analytics: React.ComponentType | null = null; if (requestedE2EHarness === 'thread-auto-update') { e2eHarness = (await import('./e2e/thread-auto-update-harness')).default; @@ -40,13 +41,16 @@ const renderRoot = async () => { } const App = (await import('./app')).default; + if (shouldLoadAnalytics) { + Analytics = (await import('@vercel/analytics/react')).Analytics; + } root.render( - {isVercelDeployment && } + {Analytics && } , ); diff --git a/src/lib/__tests__/app-update.test.ts b/src/lib/__tests__/app-update.test.ts index 10b4a215..ccc96e45 100644 --- a/src/lib/__tests__/app-update.test.ts +++ b/src/lib/__tests__/app-update.test.ts @@ -43,6 +43,7 @@ describe('app-update', () => { testState.electronDownloadAndInstallUpdateMock.mockReset(); testState.electronGetPlatformMock.mockReset(); testState.fetchMock.mockReset(); + vi.stubEnv('VITE_APP_VERSION', '0.8.1'); vi.stubGlobal('fetch', testState.fetchMock); window.electronApi = undefined; Object.defineProperty(navigator, 'serviceWorker', { @@ -91,6 +92,27 @@ describe('app-update', () => { expect(result).toBeNull(); }); + it('disables update checks for F-Droid builds', async () => { + vi.stubEnv('VITE_APP_DISTRIBUTION', 'fdroid'); + testState.capacitorPlatform = 'android'; + + const { applyAvailableAppUpdate, isAppUpdateEnabled, resolveAvailableAppUpdate } = await loadModule(); + + await expect(resolveAvailableAppUpdate()).resolves.toBeNull(); + await expect( + applyAvailableAppUpdate({ + runtime: 'android', + targetVersion: '9.9.9', + assetName: '5chan-9.9.9.apk', + downloadUrl: 'https://github.com/bitsocialnet/5chan/releases/download/v9.9.9/5chan-9.9.9.apk', + releaseUrl: 'https://github.com/bitsocialnet/5chan/releases/tag/v9.9.9', + }), + ).rejects.toThrow('App updates are disabled for this build'); + expect(isAppUpdateEnabled).toBe(false); + expect(testState.fetchMock).not.toHaveBeenCalled(); + expect(testState.androidDownloadAndInstallUpdateMock).not.toHaveBeenCalled(); + }); + it('selects the matching electron release asset for the current desktop platform', async () => { window.electronApi = { isElectron: true, diff --git a/src/lib/app-distribution.ts b/src/lib/app-distribution.ts new file mode 100644 index 00000000..38e0af7f --- /dev/null +++ b/src/lib/app-distribution.ts @@ -0,0 +1,4 @@ +const appDistribution = import.meta.env.VITE_APP_DISTRIBUTION?.trim().toLowerCase(); +const isAppUpdateEnabled = appDistribution !== 'fdroid'; + +export { isAppUpdateEnabled }; diff --git a/src/lib/app-update.ts b/src/lib/app-update.ts index b362bb83..acc3c0fa 100644 --- a/src/lib/app-update.ts +++ b/src/lib/app-update.ts @@ -1,5 +1,5 @@ import { Capacitor } from '@capacitor/core'; -import AppUpdater from '../plugins/app-updater'; +import { isAppUpdateEnabled } from './app-distribution'; import { currentAppVersion } from './app-version'; import { getDefaultReleaseUrl, getReleaseApiUrl, isAllowedDownloadUrl } from './app-update-config'; @@ -180,6 +180,10 @@ const fetchLatestReleaseUpdate = async (runtime: Extract => { + if (!isAppUpdateEnabled) { + return null; + } + const runtime = getAppRuntime(); if (runtime === 'web') { @@ -201,6 +205,10 @@ const resolveAvailableAppUpdate = async (): Promise = }; const applyAvailableAppUpdate = async (update: AvailableAppUpdate): Promise => { + if (!isAppUpdateEnabled) { + throw new Error('App updates are disabled for this build'); + } + if (update.runtime === 'web') { await refreshServiceWorkerRegistration().catch((error) => { console.error('Failed to refresh service worker registration', error); @@ -221,6 +229,7 @@ const applyAvailableAppUpdate = async (update: AvailableAppUpdate): Promise { const configuredVersion = import.meta.env.VITE_APP_VERSION; @@ -7,7 +5,7 @@ const resolveCurrentAppVersion = (): string => { return configuredVersion.trim(); } - return packageJson.version; + return '0.0.0'; }; const currentAppVersion = resolveCurrentAppVersion(); diff --git a/vite.config.js b/vite.config.js index e3d70e9a..ba72e8fc 100644 --- a/vite.config.js +++ b/vite.config.js @@ -7,6 +7,7 @@ import { VitePWA } from 'vite-plugin-pwa'; const { version: packageVersion } = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')); const appVersion = `${process.env.VITE_APP_VERSION || packageVersion}`.trim() || packageVersion; +process.env.VITE_APP_VERSION = appVersion; const publicBase = process.env.PUBLIC_URL || '/'; const buildOutDir = 'build'; const basePathPrefix = (() => { @@ -373,6 +374,7 @@ export default defineConfig({ include: ['ethers', 'assert', 'buffer', 'process', 'util', 'stream-browserify', 'isomorphic-fetch', 'workbox-core', 'workbox-precaching'], }, define: { + 'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion), 'process.env.VITE_COMMIT_REF': JSON.stringify(process.env.COMMIT_REF), 'process.version': JSON.stringify(''), global: 'globalThis',