Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 46 additions & 9 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,7 @@ <h1 id="assistantName">Assistant</h1>
<span id="statusText">Connecting to OpenClaw gateway…</span>
</div>
<div class="version-info" id="versionInfo">v0.0.0</div>
<div class="status-text" id="nativeBuildMarker" style="display:none"></div>
<button class="icon-btn" id="manualUpdateBtn" title="Check for OTA update" aria-label="Check for OTA update" style="display:none">⬇</button>
</div>
<div class="header-controls">
Expand Down Expand Up @@ -2216,6 +2217,7 @@ <h1 id="assistantName">Assistant</h1>
if (fromQuery) {
const normalized = normalizeBaseUrl(fromQuery);
localStorage.setItem(API_BASE_STORAGE_KEY, normalized);
updateNativeBuildMarker();
return normalized;
}
return normalizeBaseUrl(localStorage.getItem(API_BASE_STORAGE_KEY) || '');
Expand All @@ -2225,6 +2227,7 @@ <h1 id="assistantName">Assistant</h1>
const normalized = normalizeBaseUrl(url);
if (normalized) localStorage.setItem(API_BASE_STORAGE_KEY, normalized);
else localStorage.removeItem(API_BASE_STORAGE_KEY);
updateNativeBuildMarker();
return normalized;
}

Expand Down Expand Up @@ -2393,16 +2396,30 @@ <h1 id="assistantName">Assistant</h1>

async function ensureMobileBackendConfigured() {
const hasBackendInQuery = new URLSearchParams(window.location.search).has('backend');
if (hasBackendInQuery) return;
if (!isLikelyMobile()) return;
if (getApiBaseUrl()) return;
if (hasBackendInQuery) return true;
if (!isNativeCapacitor()) return true;
if (getApiBaseUrl()) return true;

while (true) {
const entered = window.prompt(`Enter your Miso server URL (e.g. ${SANITIZED_SERVER_EXAMPLE_URL})`, SANITIZED_SERVER_EXAMPLE_URL);
if (entered === null) {
window.alert('A backend URL is required before the APK can sign in.');
continue;
}

const entered = window.prompt(`Enter your Miso server URL (e.g. ${SANITIZED_SERVER_EXAMPLE_URL})`, SANITIZED_SERVER_EXAMPLE_URL);
if (entered === null) return;
const normalized = setApiBaseUrl(entered);
if (!normalized) {
window.alert(`That URL does not look valid. Enter a full URL like ${SANITIZED_SERVER_EXAMPLE_URL}`);
continue;
}

const normalized = setApiBaseUrl(entered);
if (!normalized) {
window.alert(`That URL does not look valid. Open the app again and enter a full URL like ${SANITIZED_SERVER_EXAMPLE_URL}`);
const result = await verifyBackendUrl(normalized);
if (!result.ok) {
window.alert(result.message || 'Backend verification failed. Please try again.');
continue;
}

return true;
}
}

Expand Down Expand Up @@ -2891,6 +2908,21 @@ <h1 id="assistantName">Assistant</h1>
}
}

function updateNativeBuildMarker() {
const marker = document.getElementById('nativeBuildMarker');
if (!marker) return;
if (!isNativeCapacitor()) {
marker.style.display = 'none';
marker.textContent = '';
return;
}
const backend = getApiBaseUrl() || '(unset)';
const updater = window.CapacitorUpdater || window.CapgoUpdater || null;
const updaterState = updater ? 'ota-plugin:yes' : 'ota-plugin:no';
marker.style.display = 'block';
marker.textContent = `native shell • backend ${backend} • ${updaterState}`;
}

function updateDocumentTitle() {
// Stop any existing blink
if (titleBlinkInterval) {
Expand Down Expand Up @@ -4241,14 +4273,19 @@ <h1 id="assistantName">Assistant</h1>

(async () => {
initTheme();
updateNativeBuildMarker();
await registerNotificationServiceWorker();
updateSoundToggle();
updateDesktopNotificationToggle();
assistantNameEl.textContent = configuredAssistantName;
messageInput.placeholder = `Message ${configuredAssistantName}...`;
resizeMessageInput();
setOnlineState(false, 'Connecting...');
await ensureMobileBackendConfigured();
const backendReady = await ensureMobileBackendConfigured();
if (!backendReady) {
setOnlineState(false, 'Backend setup required');
return;
}

// Handle deep links on mobile (when app is opened via misochat:// URL)
if (isNativeCapacitor()) {
Expand Down
9 changes: 9 additions & 0 deletions public/mobile/update-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@
checkForUpdate: async function() {
if (!this.isNativePlatform()) return { available: false, reason: 'not-native' };

const updater = getUpdater();
if (!updater || typeof updater.download !== 'function' || typeof updater.set !== 'function') {
return { available: false, reason: 'updater-unavailable' };
}

try {
this.currentBundle = await this.getCurrentBundle();
const currentVersion = this.currentBundle?.version || this.currentBundle?.id || '0.0.0';
Expand Down Expand Up @@ -218,6 +223,10 @@
}

const updater = getUpdater();
if (!updater) {
this.log('Updater plugin unavailable; skipping updater init');
return;
}
if (updater?.notifyAppReady) {
try {
await updater.notifyAppReady();
Expand Down
58 changes: 58 additions & 0 deletions tests/mobile-apk-flow-regression.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');

const indexHtmlPath = path.join(__dirname, '..', 'public', 'index.html');
const updateManagerPath = path.join(__dirname, '..', 'public', 'mobile', 'update-manager.js');

function read(filePath) {
return fs.readFileSync(filePath, 'utf8');
}

test('native onboarding requires backend configuration before app boot continues', () => {
const indexHtml = read(indexHtmlPath);

assert.match(indexHtml, /if \(!isNativeCapacitor\(\)\) return true;/);
assert.match(indexHtml, /if \(getApiBaseUrl\(\)\) return true;/);
assert.match(indexHtml, /window\.prompt\(`Enter your Miso server URL/);
assert.match(indexHtml, /window\.alert\('A backend URL is required before the APK can sign in\.'/);
assert.match(indexHtml, /const result = await verifyBackendUrl\(normalized\);/);
assert.match(indexHtml, /const backendReady = await ensureMobileBackendConfigured\(\);/);
assert.match(indexHtml, /if \(!backendReady\) \{/);
});

test('auth-required fetch path will not immediately relaunch login during callback settle window', () => {
const indexHtml = read(indexHtmlPath);

assert.match(indexHtml, /if \(mobileAuthInFlight\) \{/);
assert.match(indexHtml, /throw new Error\('Authentication pending'\);/);
assert.match(indexHtml, /const justSettled = mobileAuthSettledAt && \(Date\.now\(\) - mobileAuthSettledAt\) < 4000;/);
assert.match(indexHtml, /throw new Error\('Authentication settling'\);/);
assert.match(indexHtml, /mobileAuthSettledAt = Date\.now\(\);/);
});

test('manual OTA affordance still exists in the native shell header', () => {
const indexHtml = read(indexHtmlPath);

assert.match(indexHtml, /id="manualUpdateBtn"/);
assert.match(indexHtml, /title="Check for OTA update"/);
});

test('mobile updater fails explicitly when updater plugin is unavailable', () => {
const updateManager = read(updateManagerPath);

assert.match(updateManager, /return \{ available: false, reason: 'updater-unavailable' \};/);
assert.match(updateManager, /Updater plugin unavailable; skipping updater init/);
});


test('native build marker exposes backend and OTA plugin state in-app', () => {
const indexHtml = read(indexHtmlPath);

assert.match(indexHtml, /id="nativeBuildMarker"/);
assert.match(indexHtml, /native shell • backend/);
assert.match(indexHtml, /ota-plugin:yes/);
assert.match(indexHtml, /ota-plugin:no/);
assert.match(indexHtml, /updateNativeBuildMarker\(\);/);
});