fix: resolve Android APK download URLs for mobile updates
Prefers a real APK asset when the update API points to an AAB Keeps direct APK links from the API unchanged Uses the GitHub releases API as a fallback for Android mobile downloads
This commit is contained in:
@@ -13,6 +13,7 @@ const PACKAGE_PATH_SEGMENTS = PACKAGE_NAME.split('/');
|
|||||||
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}`;
|
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}`;
|
||||||
const CHANGELOG_URL = 'https://raw.githubusercontent.com/openchamber/openchamber/main/CHANGELOG.md';
|
const CHANGELOG_URL = 'https://raw.githubusercontent.com/openchamber/openchamber/main/CHANGELOG.md';
|
||||||
const GITHUB_RELEASES_URL = 'https://github.com/openchamber/openchamber/releases';
|
const GITHUB_RELEASES_URL = 'https://github.com/openchamber/openchamber/releases';
|
||||||
|
const GITHUB_RELEASES_API_URL = 'https://api.github.com/repos/openchamber/openchamber/releases';
|
||||||
let cachedDetectedPm = null;
|
let cachedDetectedPm = null;
|
||||||
|
|
||||||
function getSpawnSyncBaseOptions() {
|
function getSpawnSyncBaseOptions() {
|
||||||
@@ -85,6 +86,40 @@ function normalizeArch(value) {
|
|||||||
return mapArch(process.arch);
|
return mapArch(process.arch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveAndroidApkUrl(version, candidateUrl) {
|
||||||
|
if (typeof candidateUrl === 'string') {
|
||||||
|
try {
|
||||||
|
if (new URL(candidateUrl).pathname.toLowerCase().endsWith('.apk')) return candidateUrl;
|
||||||
|
} catch {
|
||||||
|
// Resolve malformed or non-APK values from the authoritative release assets below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${GITHUB_RELEASES_API_URL}/tags/v${version}`, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.github+json',
|
||||||
|
'User-Agent': 'openchamber-update-check',
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
});
|
||||||
|
if (!response.ok) return undefined;
|
||||||
|
|
||||||
|
const release = await response.json();
|
||||||
|
const apkAssets = Array.isArray(release?.assets)
|
||||||
|
? release.assets.filter((asset) => (
|
||||||
|
typeof asset?.name === 'string'
|
||||||
|
&& asset.name.toLowerCase().endsWith('.apk')
|
||||||
|
&& typeof asset.browser_download_url === 'string'
|
||||||
|
))
|
||||||
|
: [];
|
||||||
|
const canonicalAsset = apkAssets.find((asset) => /^OpenChamber-.+-android\.apk$/i.test(asset.name));
|
||||||
|
return (canonicalAsset || apkAssets[0])?.browser_download_url;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkForUpdatesFromApi(currentVersion, options = {}) {
|
async function checkForUpdatesFromApi(currentVersion, options = {}) {
|
||||||
try {
|
try {
|
||||||
const appType = normalizeAppType(options.appType);
|
const appType = normalizeAppType(options.appType);
|
||||||
@@ -128,13 +163,17 @@ async function checkForUpdatesFromApi(currentVersion, options = {}) {
|
|||||||
: typeof data.download?.url === 'string'
|
: typeof data.download?.url === 'string'
|
||||||
? data.download.url
|
? data.download.url
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const updateAvailable = Boolean(data.updateAvailable) && versionComparison > 0;
|
||||||
|
const mobileDownloadUrl = updateAvailable && appType === 'mobile-capacitor' && platform === 'android'
|
||||||
|
? await resolveAndroidApkUrl(data.latestVersion, downloadUrl)
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
available: Boolean(data.updateAvailable) && versionComparison > 0,
|
available: updateAvailable,
|
||||||
version: data.latestVersion,
|
version: data.latestVersion,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
body: typeof data.releaseNotes === 'string' ? data.releaseNotes : undefined,
|
body: typeof data.releaseNotes === 'string' ? data.releaseNotes : undefined,
|
||||||
releaseUrl: typeof data.releaseNotesUrl === 'string' ? data.releaseNotesUrl : releaseUrl,
|
releaseUrl: typeof data.releaseNotesUrl === 'string' ? data.releaseNotesUrl : releaseUrl,
|
||||||
downloadUrl: appType === 'mobile-capacitor' ? (downloadUrl || releaseUrl) : undefined,
|
downloadUrl: mobileDownloadUrl,
|
||||||
nextSuggestedCheckInSec:
|
nextSuggestedCheckInSec:
|
||||||
typeof data.nextSuggestedCheckInSec === 'number' && Number.isFinite(data.nextSuggestedCheckInSec)
|
typeof data.nextSuggestedCheckInSec === 'number' && Number.isFinite(data.nextSuggestedCheckInSec)
|
||||||
? data.nextSuggestedCheckInSec
|
? data.nextSuggestedCheckInSec
|
||||||
@@ -762,8 +801,12 @@ export async function checkForUpdates(options = {}) {
|
|||||||
|
|
||||||
const available = compareVersions(latestVersion, currentVersion) > 0;
|
const available = compareVersions(latestVersion, currentVersion) > 0;
|
||||||
let changelog;
|
let changelog;
|
||||||
|
let downloadUrl;
|
||||||
if (available) {
|
if (available) {
|
||||||
changelog = await fetchChangelogNotes(currentVersion, latestVersion);
|
changelog = await fetchChangelogNotes(currentVersion, latestVersion);
|
||||||
|
if (appType === 'mobile-capacitor' && platform === 'android') {
|
||||||
|
downloadUrl = await resolveAndroidApkUrl(latestVersion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -772,7 +815,7 @@ export async function checkForUpdates(options = {}) {
|
|||||||
currentVersion,
|
currentVersion,
|
||||||
body: changelog,
|
body: changelog,
|
||||||
releaseUrl: `${GITHUB_RELEASES_URL}/tag/v${latestVersion}`,
|
releaseUrl: `${GITHUB_RELEASES_URL}/tag/v${latestVersion}`,
|
||||||
downloadUrl: appType === 'mobile-capacitor' && platform === 'android' ? `${GITHUB_RELEASES_URL}/tag/v${latestVersion}` : undefined,
|
downloadUrl,
|
||||||
packageManager: pm,
|
packageManager: pm,
|
||||||
// Show our CLI command, not raw package manager command
|
// Show our CLI command, not raw package manager command
|
||||||
updateCommand: 'openchamber update',
|
updateCommand: 'openchamber update',
|
||||||
|
|||||||
@@ -141,6 +141,66 @@ describe('checkForUpdates', () => {
|
|||||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolves an Android APK asset when the update API returns an AAB', async () => {
|
||||||
|
fetchMock
|
||||||
|
.when('api.openchamber.dev', {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
latestVersion: '1.10.0',
|
||||||
|
updateAvailable: true,
|
||||||
|
downloadUrl: 'https://github.com/openchamber/openchamber/releases/download/v1.10.0/OpenChamber-1.10.0-42-android.aab',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.when('api.github.com/repos/openchamber/openchamber/releases/tags/v1.10.0', {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
assets: [
|
||||||
|
{
|
||||||
|
name: 'OpenChamber-1.10.0-42-android.aab',
|
||||||
|
browser_download_url: 'https://downloads.example/OpenChamber-1.10.0-42-android.aab',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'app-release.apk',
|
||||||
|
browser_download_url: 'https://downloads.example/app-release.apk',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OpenChamber-1.10.0-42-android.apk',
|
||||||
|
browser_download_url: 'https://downloads.example/OpenChamber-1.10.0-42-android.apk',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await checkForUpdates({
|
||||||
|
appType: 'mobile-capacitor',
|
||||||
|
platform: 'android',
|
||||||
|
currentVersion: '1.9.10',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.downloadUrl).toBe('https://downloads.example/OpenChamber-1.10.0-42-android.apk');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a direct Android APK URL from the update API', async () => {
|
||||||
|
const apkUrl = 'https://github.com/openchamber/openchamber/releases/download/v1.10.0/OpenChamber-1.10.0-42-android.apk';
|
||||||
|
fetchMock.when('api.openchamber.dev', {
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
latestVersion: '1.10.0',
|
||||||
|
updateAvailable: true,
|
||||||
|
downloadUrl: apkUrl,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await checkForUpdates({
|
||||||
|
appType: 'mobile-capacitor',
|
||||||
|
platform: 'android',
|
||||||
|
currentVersion: '1.9.10',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.downloadUrl).toBe(apkUrl);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns available=false when API claims update but npm is behind', async () => {
|
it('returns available=false when API claims update but npm is behind', async () => {
|
||||||
fetchMock
|
fetchMock
|
||||||
.when('api.openchamber.dev', {
|
.when('api.openchamber.dev', {
|
||||||
|
|||||||
Reference in New Issue
Block a user