mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
build(android): add fdroid distribution flavor
This commit is contained in:
+18
-4
@@ -62,6 +62,13 @@ const render = (children: React.ReactNode) => {
|
||||
});
|
||||
};
|
||||
|
||||
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<HTMLAnchorElement>('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();
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<button type='button' onClick={handleUpdateAction} disabled={isBusy}>
|
||||
{capitalize(buttonLabel)}
|
||||
</button>
|
||||
{isCheckingForUpdate && (
|
||||
<span className={styles.updateStatus} aria-live='polite'>
|
||||
{t('checking_for_updates')}
|
||||
</span>
|
||||
)}
|
||||
{!isCheckingForUpdate && availableUpdate && (
|
||||
<span className={styles.updateStatus} aria-live='polite'>
|
||||
{t('new_version_found')}:
|
||||
<a href={availableUpdate.releaseUrl} target='_blank' rel='noopener noreferrer'>
|
||||
v{availableUpdate.targetVersion}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AppUpdateSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={styles.setting}>
|
||||
{capitalize(t('update'))}: <UpdateButton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppUpdateSetting;
|
||||
@@ -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 (
|
||||
<>
|
||||
<button type='button' onClick={handleUpdateAction} disabled={isBusy}>
|
||||
{capitalize(buttonLabel)}
|
||||
</button>
|
||||
{isCheckingForUpdate && (
|
||||
<span className={styles.updateStatus} aria-live='polite'>
|
||||
{t('checking_for_updates')}
|
||||
</span>
|
||||
)}
|
||||
{!isCheckingForUpdate && availableUpdate && (
|
||||
<span className={styles.updateStatus} aria-live='polite'>
|
||||
{t('new_version_found')}:
|
||||
<a href={availableUpdate.releaseUrl} target='_blank' rel='noopener noreferrer'>
|
||||
v{availableUpdate.targetVersion}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
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 = () => {
|
||||
<div className={styles.version}>
|
||||
{capitalize(t('version'))}: <Version />
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
{capitalize(t('update'))}: <UpdateButton />
|
||||
</div>
|
||||
{AppUpdateSetting && (
|
||||
<Suspense fallback={null}>
|
||||
<AppUpdateSetting />
|
||||
</Suspense>
|
||||
)}
|
||||
<div className={styles.setting}>
|
||||
{capitalize(t('interface_language'))}: <InterfaceLanguage />
|
||||
</div>
|
||||
|
||||
Vendored
+2
@@ -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 {
|
||||
|
||||
+6
-2
@@ -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(
|
||||
<React.StrictMode>
|
||||
<Router>
|
||||
<AppUpdateRegistration />
|
||||
<App />
|
||||
{isVercelDeployment && <Analytics />}
|
||||
{Analytics && <Analytics />}
|
||||
</Router>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
const appDistribution = import.meta.env.VITE_APP_DISTRIBUTION?.trim().toLowerCase();
|
||||
const isAppUpdateEnabled = appDistribution !== 'fdroid';
|
||||
|
||||
export { isAppUpdateEnabled };
|
||||
+11
-2
@@ -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<AppRuntime, 'electron'
|
||||
};
|
||||
|
||||
const resolveAvailableAppUpdate = async (): Promise<AvailableAppUpdate | null> => {
|
||||
if (!isAppUpdateEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const runtime = getAppRuntime();
|
||||
|
||||
if (runtime === 'web') {
|
||||
@@ -201,6 +205,10 @@ const resolveAvailableAppUpdate = async (): Promise<AvailableAppUpdate | null> =
|
||||
};
|
||||
|
||||
const applyAvailableAppUpdate = async (update: AvailableAppUpdate): Promise<void> => {
|
||||
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<void
|
||||
return;
|
||||
}
|
||||
|
||||
const { default: AppUpdater } = await import('../plugins/app-updater');
|
||||
await AppUpdater.downloadAndInstallUpdate({
|
||||
url: update.downloadUrl,
|
||||
fileName: update.assetName,
|
||||
@@ -228,4 +237,4 @@ const applyAvailableAppUpdate = async (update: AvailableAppUpdate): Promise<void
|
||||
};
|
||||
|
||||
export type { AppRuntime, AvailableAppUpdate, NativeAppUpdateInfo, WebAppUpdateInfo };
|
||||
export { applyAvailableAppUpdate, fetchLatestStableVersion, getAppRuntime, isElectron, refreshServiceWorkerRegistration, resolveAvailableAppUpdate };
|
||||
export { applyAvailableAppUpdate, fetchLatestStableVersion, getAppRuntime, isAppUpdateEnabled, isElectron, refreshServiceWorkerRegistration, resolveAvailableAppUpdate };
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import packageJson from '../../package.json';
|
||||
|
||||
const resolveCurrentAppVersion = (): string => {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user