From f03ca5cb9448ea8e30898866ef1df42984dac53f Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 11 Apr 2026 03:34:47 +0200 Subject: [PATCH] Fix Android mic permission, simplify launcher, remove broken biometric - MainActivity: request RECORD_AUDIO permission at app start via ActivityCompat (not WebChromeClient override which broke Capacitor bridge) - Simplify launcher: remove server reachability check (was failing in WebView), just save URL and navigate directly - Remove biometric auth from launcher (Capacitor plugins need ES module bundler, not available in plain HTML). Biometric can be added later via the web app with proper Capacitor runtime. - Add webContentsDebuggingEnabled for development --- .../app/src/main/assets/capacitor.config.json | 4 +- .../app/src/main/assets/public/index.html | 16 -- .../app/src/main/assets/public/launcher.js | 143 +----------------- .../java/com/pedshub/scribe/MainActivity.java | 28 ++-- mobile/capacitor.config.json | 4 +- mobile/ios/App/App/capacitor.config.json | 4 +- mobile/ios/App/App/public/index.html | 16 -- mobile/ios/App/App/public/launcher.js | 143 +----------------- mobile/src/index.html | 16 -- mobile/src/launcher.js | 143 +----------------- 10 files changed, 52 insertions(+), 465 deletions(-) diff --git a/mobile/android/app/src/main/assets/capacitor.config.json b/mobile/android/app/src/main/assets/capacitor.config.json index 75bcc7c..813d996 100644 --- a/mobile/android/app/src/main/assets/capacitor.config.json +++ b/mobile/android/app/src/main/assets/capacitor.config.json @@ -28,7 +28,9 @@ }, "android": { "allowMixedContent": true, - "backgroundColor": "#2563eb" + "backgroundColor": "#2563eb", + "webContentsDebuggingEnabled": true, + "appendUserAgent": "PedScribe-Android" }, "ios": { "backgroundColor": "#2563eb", diff --git a/mobile/android/app/src/main/assets/public/index.html b/mobile/android/app/src/main/assets/public/index.html index 2936fd2..ae6cb0c 100644 --- a/mobile/android/app/src/main/assets/public/index.html +++ b/mobile/android/app/src/main/assets/public/index.html @@ -23,22 +23,6 @@ - -
diff --git a/mobile/android/app/src/main/assets/public/launcher.js b/mobile/android/app/src/main/assets/public/launcher.js index 1c5e663..8ce1804 100644 --- a/mobile/android/app/src/main/assets/public/launcher.js +++ b/mobile/android/app/src/main/assets/public/launcher.js @@ -1,36 +1,26 @@ // PedScribe Mobile Launcher -// Handles server URL config, biometric auth, and native features +// Handles configurable server URL and auto-redirect (function() { var STORAGE_KEY = 'pedscribe_server_url'; - var BIOMETRIC_KEY = 'pedscribe_biometric_enabled'; var DEFAULT_URL = 'https://app.pedshub.com'; var setupScreen = document.getElementById('setup-screen'); var connectingScreen = document.getElementById('connecting-screen'); - var biometricScreen = document.getElementById('biometric-screen'); var urlInput = document.getElementById('server-url'); var connectBtn = document.getElementById('btn-connect'); var changeBtn = document.getElementById('btn-change-server'); - var unlockBtn = document.getElementById('btn-unlock'); - var skipBioBtn = document.getElementById('btn-skip-biometric'); var savedUrl = localStorage.getItem(STORAGE_KEY); - var biometricEnabled = localStorage.getItem(BIOMETRIC_KEY) === 'true'; - // ── Launch flow ── if (savedUrl) { - if (biometricEnabled && isCapacitor()) { - showBiometricScreen(savedUrl); - } else { - showConnecting(savedUrl); - } + showConnecting(savedUrl); } else { urlInput.value = DEFAULT_URL; showScreen('setup'); } - // ── Connect button ── + // Connect button connectBtn.addEventListener('click', function() { var url = (urlInput.value || DEFAULT_URL).trim().replace(/\/+$/, ''); if (!url.startsWith('http')) url = 'https://' + url; @@ -39,27 +29,15 @@ connectBtn.textContent = 'Connecting...'; haptic(); - testServer(url, function(ok) { - if (ok) { - localStorage.setItem(STORAGE_KEY, url); - // Enable biometric on first connect if available - checkBiometricAvailable(function(available) { - if (available) localStorage.setItem(BIOMETRIC_KEY, 'true'); - }); - navigateToServer(url); - } else { - connectBtn.disabled = false; - connectBtn.textContent = 'Connect'; - showError('Could not reach server. Check the URL and try again.'); - } - }); + localStorage.setItem(STORAGE_KEY, url); + navigateToServer(url); }); urlInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') connectBtn.click(); }); - // ── Change server ── + // Change server changeBtn.addEventListener('click', function() { localStorage.removeItem(STORAGE_KEY); urlInput.value = savedUrl || DEFAULT_URL; @@ -67,114 +45,21 @@ urlInput.focus(); }); - // ── Biometric unlock ── - if (unlockBtn) { - unlockBtn.addEventListener('click', function() { - doBiometricAuth(function(ok) { - if (ok) { - showConnecting(savedUrl); - } else { - showError('Authentication failed. Try again.'); - } - }); - }); - } - - if (skipBioBtn) { - skipBioBtn.addEventListener('click', function() { - showConnecting(savedUrl); - }); - } - - // ── Screen management ── + // Screen management function showScreen(which) { setupScreen.style.display = which === 'setup' ? '' : 'none'; connectingScreen.style.display = which === 'connecting' ? '' : 'none'; - biometricScreen.style.display = which === 'biometric' ? '' : 'none'; - } - - function showBiometricScreen(url) { - showScreen('biometric'); - // Auto-trigger biometric prompt - setTimeout(function() { - doBiometricAuth(function(ok) { - if (ok) showConnecting(url); - // If fails, user sees the manual Unlock button - }); - }, 300); } function showConnecting(url) { showScreen('connecting'); - setTimeout(function() { - testServer(url, function(ok) { - if (ok) { - navigateToServer(url); - } else { - urlInput.value = url; - showScreen('setup'); - showError('Server not reachable. Check your connection or change the URL.'); - } - }); - }, 500); - } - - // ── Server check ── - function testServer(url, callback) { - // In native WebView, no-cors fetch and image probes are unreliable. - // Just try a normal fetch to the health endpoint. - fetch(url + '/api/health', { signal: AbortSignal.timeout(8000) }) - .then(function(r) { callback(true); }) - .catch(function() { - // If CORS blocks it, that still means the server is reachable - // (CORS error = server responded but blocked the origin). - // Just navigate — the WebView will handle it. - callback(true); - }); + setTimeout(function() { navigateToServer(url); }, 800); } function navigateToServer(url) { window.location.href = url; } - // ── Biometric auth ── - function isCapacitor() { - return !!(window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform()); - } - - function checkBiometricAvailable(callback) { - if (!isCapacitor()) { callback(false); return; } - try { - var BiometricAuth = window.Capacitor.Plugins.BiometricAuth; - if (!BiometricAuth) { callback(false); return; } - BiometricAuth.checkBiometry().then(function(result) { - callback(result.isAvailable); - }).catch(function() { callback(false); }); - } catch(e) { callback(false); } - } - - function doBiometricAuth(callback) { - if (!isCapacitor()) { callback(true); return; } // Skip on web - try { - var BiometricAuth = window.Capacitor.Plugins.BiometricAuth; - if (!BiometricAuth) { callback(true); return; } - BiometricAuth.authenticate({ - reason: 'Unlock PedScribe', - cancelTitle: 'Cancel', - allowDeviceCredential: true, // Allow PIN/password fallback - iosFallbackTitle: 'Use Passcode', - androidTitle: 'PedScribe', - androidSubtitle: 'Verify your identity to continue', - androidConfirmationRequired: false - }).then(function() { - callback(true); - }).catch(function() { - callback(false); - }); - } catch(e) { callback(true); } // Fail open if plugin not available - } - - // ── Helpers ── function haptic() { try { if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) { @@ -182,16 +67,4 @@ } } catch(e) {} } - - function showError(msg) { - var existing = document.querySelector('.error-msg'); - if (existing) existing.remove(); - var div = document.createElement('div'); - div.className = 'error-msg'; - div.style.display = 'block'; - div.textContent = msg; - var parent = connectBtn ? connectBtn.parentNode : document.querySelector('.launcher'); - if (parent) parent.appendChild(div); - setTimeout(function() { if (div.parentNode) div.remove(); }, 8000); - } })(); diff --git a/mobile/android/app/src/main/java/com/pedshub/scribe/MainActivity.java b/mobile/android/app/src/main/java/com/pedshub/scribe/MainActivity.java index e3c1876..aa5b1d7 100644 --- a/mobile/android/app/src/main/java/com/pedshub/scribe/MainActivity.java +++ b/mobile/android/app/src/main/java/com/pedshub/scribe/MainActivity.java @@ -1,24 +1,34 @@ package com.pedshub.scribe; +import android.Manifest; +import android.content.pm.PackageManager; import android.os.Bundle; import android.webkit.PermissionRequest; -import android.webkit.WebChromeClient; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import com.getcapacitor.BridgeActivity; public class MainActivity extends BridgeActivity { + private static final int MIC_PERMISSION_CODE = 1001; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Auto-grant WebView permissions (microphone, camera) - // The Android runtime permission dialog still shows on first use - this.bridge.getWebView().setWebChromeClient(new WebChromeClient() { - @Override - public void onPermissionRequest(PermissionRequest request) { - request.grant(request.getResources()); - } - }); + // Request microphone permission at app start (not just when WebView asks) + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{ Manifest.permission.RECORD_AUDIO }, MIC_PERMISSION_CODE); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + // Capacitor handles the result via its bridge } } diff --git a/mobile/capacitor.config.json b/mobile/capacitor.config.json index 5d2360e..4524111 100644 --- a/mobile/capacitor.config.json +++ b/mobile/capacitor.config.json @@ -22,7 +22,9 @@ }, "android": { "allowMixedContent": true, - "backgroundColor": "#2563eb" + "backgroundColor": "#2563eb", + "webContentsDebuggingEnabled": true, + "appendUserAgent": "PedScribe-Android" }, "ios": { "backgroundColor": "#2563eb", diff --git a/mobile/ios/App/App/capacitor.config.json b/mobile/ios/App/App/capacitor.config.json index 583c488..293c1a2 100644 --- a/mobile/ios/App/App/capacitor.config.json +++ b/mobile/ios/App/App/capacitor.config.json @@ -28,7 +28,9 @@ }, "android": { "allowMixedContent": true, - "backgroundColor": "#2563eb" + "backgroundColor": "#2563eb", + "webContentsDebuggingEnabled": true, + "appendUserAgent": "PedScribe-Android" }, "ios": { "backgroundColor": "#2563eb", diff --git a/mobile/ios/App/App/public/index.html b/mobile/ios/App/App/public/index.html index 2936fd2..ae6cb0c 100644 --- a/mobile/ios/App/App/public/index.html +++ b/mobile/ios/App/App/public/index.html @@ -23,22 +23,6 @@
- -
diff --git a/mobile/ios/App/App/public/launcher.js b/mobile/ios/App/App/public/launcher.js index 1c5e663..8ce1804 100644 --- a/mobile/ios/App/App/public/launcher.js +++ b/mobile/ios/App/App/public/launcher.js @@ -1,36 +1,26 @@ // PedScribe Mobile Launcher -// Handles server URL config, biometric auth, and native features +// Handles configurable server URL and auto-redirect (function() { var STORAGE_KEY = 'pedscribe_server_url'; - var BIOMETRIC_KEY = 'pedscribe_biometric_enabled'; var DEFAULT_URL = 'https://app.pedshub.com'; var setupScreen = document.getElementById('setup-screen'); var connectingScreen = document.getElementById('connecting-screen'); - var biometricScreen = document.getElementById('biometric-screen'); var urlInput = document.getElementById('server-url'); var connectBtn = document.getElementById('btn-connect'); var changeBtn = document.getElementById('btn-change-server'); - var unlockBtn = document.getElementById('btn-unlock'); - var skipBioBtn = document.getElementById('btn-skip-biometric'); var savedUrl = localStorage.getItem(STORAGE_KEY); - var biometricEnabled = localStorage.getItem(BIOMETRIC_KEY) === 'true'; - // ── Launch flow ── if (savedUrl) { - if (biometricEnabled && isCapacitor()) { - showBiometricScreen(savedUrl); - } else { - showConnecting(savedUrl); - } + showConnecting(savedUrl); } else { urlInput.value = DEFAULT_URL; showScreen('setup'); } - // ── Connect button ── + // Connect button connectBtn.addEventListener('click', function() { var url = (urlInput.value || DEFAULT_URL).trim().replace(/\/+$/, ''); if (!url.startsWith('http')) url = 'https://' + url; @@ -39,27 +29,15 @@ connectBtn.textContent = 'Connecting...'; haptic(); - testServer(url, function(ok) { - if (ok) { - localStorage.setItem(STORAGE_KEY, url); - // Enable biometric on first connect if available - checkBiometricAvailable(function(available) { - if (available) localStorage.setItem(BIOMETRIC_KEY, 'true'); - }); - navigateToServer(url); - } else { - connectBtn.disabled = false; - connectBtn.textContent = 'Connect'; - showError('Could not reach server. Check the URL and try again.'); - } - }); + localStorage.setItem(STORAGE_KEY, url); + navigateToServer(url); }); urlInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') connectBtn.click(); }); - // ── Change server ── + // Change server changeBtn.addEventListener('click', function() { localStorage.removeItem(STORAGE_KEY); urlInput.value = savedUrl || DEFAULT_URL; @@ -67,114 +45,21 @@ urlInput.focus(); }); - // ── Biometric unlock ── - if (unlockBtn) { - unlockBtn.addEventListener('click', function() { - doBiometricAuth(function(ok) { - if (ok) { - showConnecting(savedUrl); - } else { - showError('Authentication failed. Try again.'); - } - }); - }); - } - - if (skipBioBtn) { - skipBioBtn.addEventListener('click', function() { - showConnecting(savedUrl); - }); - } - - // ── Screen management ── + // Screen management function showScreen(which) { setupScreen.style.display = which === 'setup' ? '' : 'none'; connectingScreen.style.display = which === 'connecting' ? '' : 'none'; - biometricScreen.style.display = which === 'biometric' ? '' : 'none'; - } - - function showBiometricScreen(url) { - showScreen('biometric'); - // Auto-trigger biometric prompt - setTimeout(function() { - doBiometricAuth(function(ok) { - if (ok) showConnecting(url); - // If fails, user sees the manual Unlock button - }); - }, 300); } function showConnecting(url) { showScreen('connecting'); - setTimeout(function() { - testServer(url, function(ok) { - if (ok) { - navigateToServer(url); - } else { - urlInput.value = url; - showScreen('setup'); - showError('Server not reachable. Check your connection or change the URL.'); - } - }); - }, 500); - } - - // ── Server check ── - function testServer(url, callback) { - // In native WebView, no-cors fetch and image probes are unreliable. - // Just try a normal fetch to the health endpoint. - fetch(url + '/api/health', { signal: AbortSignal.timeout(8000) }) - .then(function(r) { callback(true); }) - .catch(function() { - // If CORS blocks it, that still means the server is reachable - // (CORS error = server responded but blocked the origin). - // Just navigate — the WebView will handle it. - callback(true); - }); + setTimeout(function() { navigateToServer(url); }, 800); } function navigateToServer(url) { window.location.href = url; } - // ── Biometric auth ── - function isCapacitor() { - return !!(window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform()); - } - - function checkBiometricAvailable(callback) { - if (!isCapacitor()) { callback(false); return; } - try { - var BiometricAuth = window.Capacitor.Plugins.BiometricAuth; - if (!BiometricAuth) { callback(false); return; } - BiometricAuth.checkBiometry().then(function(result) { - callback(result.isAvailable); - }).catch(function() { callback(false); }); - } catch(e) { callback(false); } - } - - function doBiometricAuth(callback) { - if (!isCapacitor()) { callback(true); return; } // Skip on web - try { - var BiometricAuth = window.Capacitor.Plugins.BiometricAuth; - if (!BiometricAuth) { callback(true); return; } - BiometricAuth.authenticate({ - reason: 'Unlock PedScribe', - cancelTitle: 'Cancel', - allowDeviceCredential: true, // Allow PIN/password fallback - iosFallbackTitle: 'Use Passcode', - androidTitle: 'PedScribe', - androidSubtitle: 'Verify your identity to continue', - androidConfirmationRequired: false - }).then(function() { - callback(true); - }).catch(function() { - callback(false); - }); - } catch(e) { callback(true); } // Fail open if plugin not available - } - - // ── Helpers ── function haptic() { try { if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) { @@ -182,16 +67,4 @@ } } catch(e) {} } - - function showError(msg) { - var existing = document.querySelector('.error-msg'); - if (existing) existing.remove(); - var div = document.createElement('div'); - div.className = 'error-msg'; - div.style.display = 'block'; - div.textContent = msg; - var parent = connectBtn ? connectBtn.parentNode : document.querySelector('.launcher'); - if (parent) parent.appendChild(div); - setTimeout(function() { if (div.parentNode) div.remove(); }, 8000); - } })(); diff --git a/mobile/src/index.html b/mobile/src/index.html index 2936fd2..ae6cb0c 100644 --- a/mobile/src/index.html +++ b/mobile/src/index.html @@ -23,22 +23,6 @@
- -
diff --git a/mobile/src/launcher.js b/mobile/src/launcher.js index 1c5e663..8ce1804 100644 --- a/mobile/src/launcher.js +++ b/mobile/src/launcher.js @@ -1,36 +1,26 @@ // PedScribe Mobile Launcher -// Handles server URL config, biometric auth, and native features +// Handles configurable server URL and auto-redirect (function() { var STORAGE_KEY = 'pedscribe_server_url'; - var BIOMETRIC_KEY = 'pedscribe_biometric_enabled'; var DEFAULT_URL = 'https://app.pedshub.com'; var setupScreen = document.getElementById('setup-screen'); var connectingScreen = document.getElementById('connecting-screen'); - var biometricScreen = document.getElementById('biometric-screen'); var urlInput = document.getElementById('server-url'); var connectBtn = document.getElementById('btn-connect'); var changeBtn = document.getElementById('btn-change-server'); - var unlockBtn = document.getElementById('btn-unlock'); - var skipBioBtn = document.getElementById('btn-skip-biometric'); var savedUrl = localStorage.getItem(STORAGE_KEY); - var biometricEnabled = localStorage.getItem(BIOMETRIC_KEY) === 'true'; - // ── Launch flow ── if (savedUrl) { - if (biometricEnabled && isCapacitor()) { - showBiometricScreen(savedUrl); - } else { - showConnecting(savedUrl); - } + showConnecting(savedUrl); } else { urlInput.value = DEFAULT_URL; showScreen('setup'); } - // ── Connect button ── + // Connect button connectBtn.addEventListener('click', function() { var url = (urlInput.value || DEFAULT_URL).trim().replace(/\/+$/, ''); if (!url.startsWith('http')) url = 'https://' + url; @@ -39,27 +29,15 @@ connectBtn.textContent = 'Connecting...'; haptic(); - testServer(url, function(ok) { - if (ok) { - localStorage.setItem(STORAGE_KEY, url); - // Enable biometric on first connect if available - checkBiometricAvailable(function(available) { - if (available) localStorage.setItem(BIOMETRIC_KEY, 'true'); - }); - navigateToServer(url); - } else { - connectBtn.disabled = false; - connectBtn.textContent = 'Connect'; - showError('Could not reach server. Check the URL and try again.'); - } - }); + localStorage.setItem(STORAGE_KEY, url); + navigateToServer(url); }); urlInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') connectBtn.click(); }); - // ── Change server ── + // Change server changeBtn.addEventListener('click', function() { localStorage.removeItem(STORAGE_KEY); urlInput.value = savedUrl || DEFAULT_URL; @@ -67,114 +45,21 @@ urlInput.focus(); }); - // ── Biometric unlock ── - if (unlockBtn) { - unlockBtn.addEventListener('click', function() { - doBiometricAuth(function(ok) { - if (ok) { - showConnecting(savedUrl); - } else { - showError('Authentication failed. Try again.'); - } - }); - }); - } - - if (skipBioBtn) { - skipBioBtn.addEventListener('click', function() { - showConnecting(savedUrl); - }); - } - - // ── Screen management ── + // Screen management function showScreen(which) { setupScreen.style.display = which === 'setup' ? '' : 'none'; connectingScreen.style.display = which === 'connecting' ? '' : 'none'; - biometricScreen.style.display = which === 'biometric' ? '' : 'none'; - } - - function showBiometricScreen(url) { - showScreen('biometric'); - // Auto-trigger biometric prompt - setTimeout(function() { - doBiometricAuth(function(ok) { - if (ok) showConnecting(url); - // If fails, user sees the manual Unlock button - }); - }, 300); } function showConnecting(url) { showScreen('connecting'); - setTimeout(function() { - testServer(url, function(ok) { - if (ok) { - navigateToServer(url); - } else { - urlInput.value = url; - showScreen('setup'); - showError('Server not reachable. Check your connection or change the URL.'); - } - }); - }, 500); - } - - // ── Server check ── - function testServer(url, callback) { - // In native WebView, no-cors fetch and image probes are unreliable. - // Just try a normal fetch to the health endpoint. - fetch(url + '/api/health', { signal: AbortSignal.timeout(8000) }) - .then(function(r) { callback(true); }) - .catch(function() { - // If CORS blocks it, that still means the server is reachable - // (CORS error = server responded but blocked the origin). - // Just navigate — the WebView will handle it. - callback(true); - }); + setTimeout(function() { navigateToServer(url); }, 800); } function navigateToServer(url) { window.location.href = url; } - // ── Biometric auth ── - function isCapacitor() { - return !!(window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform()); - } - - function checkBiometricAvailable(callback) { - if (!isCapacitor()) { callback(false); return; } - try { - var BiometricAuth = window.Capacitor.Plugins.BiometricAuth; - if (!BiometricAuth) { callback(false); return; } - BiometricAuth.checkBiometry().then(function(result) { - callback(result.isAvailable); - }).catch(function() { callback(false); }); - } catch(e) { callback(false); } - } - - function doBiometricAuth(callback) { - if (!isCapacitor()) { callback(true); return; } // Skip on web - try { - var BiometricAuth = window.Capacitor.Plugins.BiometricAuth; - if (!BiometricAuth) { callback(true); return; } - BiometricAuth.authenticate({ - reason: 'Unlock PedScribe', - cancelTitle: 'Cancel', - allowDeviceCredential: true, // Allow PIN/password fallback - iosFallbackTitle: 'Use Passcode', - androidTitle: 'PedScribe', - androidSubtitle: 'Verify your identity to continue', - androidConfirmationRequired: false - }).then(function() { - callback(true); - }).catch(function() { - callback(false); - }); - } catch(e) { callback(true); } // Fail open if plugin not available - } - - // ── Helpers ── function haptic() { try { if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) { @@ -182,16 +67,4 @@ } } catch(e) {} } - - function showError(msg) { - var existing = document.querySelector('.error-msg'); - if (existing) existing.remove(); - var div = document.createElement('div'); - div.className = 'error-msg'; - div.style.display = 'block'; - div.textContent = msg; - var parent = connectBtn ? connectBtn.parentNode : document.querySelector('.launcher'); - if (parent) parent.appendChild(div); - setTimeout(function() { if (div.parentNode) div.remove(); }, 8000); - } })();