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
This commit is contained in:
parent
6978ed708c
commit
f03ca5cb94
10 changed files with 52 additions and 465 deletions
|
|
@ -28,7 +28,9 @@
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"allowMixedContent": true,
|
"allowMixedContent": true,
|
||||||
"backgroundColor": "#2563eb"
|
"backgroundColor": "#2563eb",
|
||||||
|
"webContentsDebuggingEnabled": true,
|
||||||
|
"appendUserAgent": "PedScribe-Android"
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"backgroundColor": "#2563eb",
|
"backgroundColor": "#2563eb",
|
||||||
|
|
|
||||||
|
|
@ -23,22 +23,6 @@
|
||||||
<button id="btn-change-server" class="btn-link">Change Server</button>
|
<button id="btn-change-server" class="btn-link">Change Server</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Biometric lock screen -->
|
|
||||||
<div id="biometric-screen" style="display:none;">
|
|
||||||
<div class="logo-icon">
|
|
||||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="24" cy="24" r="22" fill="white" fill-opacity="0.15"/>
|
|
||||||
<path d="M24 12c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4 4s4-1.8 4-4V16c0-2.2-1.8-4-4-4z" fill="white"/>
|
|
||||||
<path d="M32 22v2c0 4.4-3.6 8-8 8s-8-3.6-8-8v-2h-2v2c0 5.1 3.8 9.3 8.7 9.9V36H20v2h8v-2h-2.7v-2.1c4.9-.6 8.7-4.8 8.7-9.9v-2h-2z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h1>PedScribe</h1>
|
|
||||||
<p class="subtitle">Unlock to continue</p>
|
|
||||||
<button id="btn-unlock" class="btn-primary" style="margin-top:20px;">
|
|
||||||
Unlock with Biometrics
|
|
||||||
</button>
|
|
||||||
<button id="btn-skip-biometric" class="btn-link">Skip</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Server URL setup screen -->
|
<!-- Server URL setup screen -->
|
||||||
<div id="setup-screen">
|
<div id="setup-screen">
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,26 @@
|
||||||
// PedScribe Mobile Launcher
|
// PedScribe Mobile Launcher
|
||||||
// Handles server URL config, biometric auth, and native features
|
// Handles configurable server URL and auto-redirect
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
var STORAGE_KEY = 'pedscribe_server_url';
|
var STORAGE_KEY = 'pedscribe_server_url';
|
||||||
var BIOMETRIC_KEY = 'pedscribe_biometric_enabled';
|
|
||||||
var DEFAULT_URL = 'https://app.pedshub.com';
|
var DEFAULT_URL = 'https://app.pedshub.com';
|
||||||
|
|
||||||
var setupScreen = document.getElementById('setup-screen');
|
var setupScreen = document.getElementById('setup-screen');
|
||||||
var connectingScreen = document.getElementById('connecting-screen');
|
var connectingScreen = document.getElementById('connecting-screen');
|
||||||
var biometricScreen = document.getElementById('biometric-screen');
|
|
||||||
var urlInput = document.getElementById('server-url');
|
var urlInput = document.getElementById('server-url');
|
||||||
var connectBtn = document.getElementById('btn-connect');
|
var connectBtn = document.getElementById('btn-connect');
|
||||||
var changeBtn = document.getElementById('btn-change-server');
|
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 savedUrl = localStorage.getItem(STORAGE_KEY);
|
||||||
var biometricEnabled = localStorage.getItem(BIOMETRIC_KEY) === 'true';
|
|
||||||
|
|
||||||
// ── Launch flow ──
|
|
||||||
if (savedUrl) {
|
if (savedUrl) {
|
||||||
if (biometricEnabled && isCapacitor()) {
|
showConnecting(savedUrl);
|
||||||
showBiometricScreen(savedUrl);
|
|
||||||
} else {
|
|
||||||
showConnecting(savedUrl);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
urlInput.value = DEFAULT_URL;
|
urlInput.value = DEFAULT_URL;
|
||||||
showScreen('setup');
|
showScreen('setup');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Connect button ──
|
// Connect button
|
||||||
connectBtn.addEventListener('click', function() {
|
connectBtn.addEventListener('click', function() {
|
||||||
var url = (urlInput.value || DEFAULT_URL).trim().replace(/\/+$/, '');
|
var url = (urlInput.value || DEFAULT_URL).trim().replace(/\/+$/, '');
|
||||||
if (!url.startsWith('http')) url = 'https://' + url;
|
if (!url.startsWith('http')) url = 'https://' + url;
|
||||||
|
|
@ -39,27 +29,15 @@
|
||||||
connectBtn.textContent = 'Connecting...';
|
connectBtn.textContent = 'Connecting...';
|
||||||
haptic();
|
haptic();
|
||||||
|
|
||||||
testServer(url, function(ok) {
|
localStorage.setItem(STORAGE_KEY, url);
|
||||||
if (ok) {
|
navigateToServer(url);
|
||||||
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.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
urlInput.addEventListener('keydown', function(e) {
|
urlInput.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Enter') connectBtn.click();
|
if (e.key === 'Enter') connectBtn.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Change server ──
|
// Change server
|
||||||
changeBtn.addEventListener('click', function() {
|
changeBtn.addEventListener('click', function() {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
urlInput.value = savedUrl || DEFAULT_URL;
|
urlInput.value = savedUrl || DEFAULT_URL;
|
||||||
|
|
@ -67,114 +45,21 @@
|
||||||
urlInput.focus();
|
urlInput.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Biometric unlock ──
|
// Screen management
|
||||||
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 ──
|
|
||||||
function showScreen(which) {
|
function showScreen(which) {
|
||||||
setupScreen.style.display = which === 'setup' ? '' : 'none';
|
setupScreen.style.display = which === 'setup' ? '' : 'none';
|
||||||
connectingScreen.style.display = which === 'connecting' ? '' : '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) {
|
function showConnecting(url) {
|
||||||
showScreen('connecting');
|
showScreen('connecting');
|
||||||
setTimeout(function() {
|
setTimeout(function() { navigateToServer(url); }, 800);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateToServer(url) {
|
function navigateToServer(url) {
|
||||||
window.location.href = 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() {
|
function haptic() {
|
||||||
try {
|
try {
|
||||||
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) {
|
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) {
|
||||||
|
|
@ -182,16 +67,4 @@
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} 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);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,34 @@
|
||||||
package com.pedshub.scribe;
|
package com.pedshub.scribe;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.webkit.PermissionRequest;
|
import android.webkit.PermissionRequest;
|
||||||
import android.webkit.WebChromeClient;
|
|
||||||
|
import androidx.core.app.ActivityCompat;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import com.getcapacitor.BridgeActivity;
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {
|
public class MainActivity extends BridgeActivity {
|
||||||
|
|
||||||
|
private static final int MIC_PERMISSION_CODE = 1001;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
// Auto-grant WebView permissions (microphone, camera)
|
// Request microphone permission at app start (not just when WebView asks)
|
||||||
// The Android runtime permission dialog still shows on first use
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
|
||||||
this.bridge.getWebView().setWebChromeClient(new WebChromeClient() {
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
@Override
|
ActivityCompat.requestPermissions(this,
|
||||||
public void onPermissionRequest(PermissionRequest request) {
|
new String[]{ Manifest.permission.RECORD_AUDIO }, MIC_PERMISSION_CODE);
|
||||||
request.grant(request.getResources());
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
// Capacitor handles the result via its bridge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"allowMixedContent": true,
|
"allowMixedContent": true,
|
||||||
"backgroundColor": "#2563eb"
|
"backgroundColor": "#2563eb",
|
||||||
|
"webContentsDebuggingEnabled": true,
|
||||||
|
"appendUserAgent": "PedScribe-Android"
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"backgroundColor": "#2563eb",
|
"backgroundColor": "#2563eb",
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"allowMixedContent": true,
|
"allowMixedContent": true,
|
||||||
"backgroundColor": "#2563eb"
|
"backgroundColor": "#2563eb",
|
||||||
|
"webContentsDebuggingEnabled": true,
|
||||||
|
"appendUserAgent": "PedScribe-Android"
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"backgroundColor": "#2563eb",
|
"backgroundColor": "#2563eb",
|
||||||
|
|
|
||||||
|
|
@ -23,22 +23,6 @@
|
||||||
<button id="btn-change-server" class="btn-link">Change Server</button>
|
<button id="btn-change-server" class="btn-link">Change Server</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Biometric lock screen -->
|
|
||||||
<div id="biometric-screen" style="display:none;">
|
|
||||||
<div class="logo-icon">
|
|
||||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="24" cy="24" r="22" fill="white" fill-opacity="0.15"/>
|
|
||||||
<path d="M24 12c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4 4s4-1.8 4-4V16c0-2.2-1.8-4-4-4z" fill="white"/>
|
|
||||||
<path d="M32 22v2c0 4.4-3.6 8-8 8s-8-3.6-8-8v-2h-2v2c0 5.1 3.8 9.3 8.7 9.9V36H20v2h8v-2h-2.7v-2.1c4.9-.6 8.7-4.8 8.7-9.9v-2h-2z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h1>PedScribe</h1>
|
|
||||||
<p class="subtitle">Unlock to continue</p>
|
|
||||||
<button id="btn-unlock" class="btn-primary" style="margin-top:20px;">
|
|
||||||
Unlock with Biometrics
|
|
||||||
</button>
|
|
||||||
<button id="btn-skip-biometric" class="btn-link">Skip</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Server URL setup screen -->
|
<!-- Server URL setup screen -->
|
||||||
<div id="setup-screen">
|
<div id="setup-screen">
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,26 @@
|
||||||
// PedScribe Mobile Launcher
|
// PedScribe Mobile Launcher
|
||||||
// Handles server URL config, biometric auth, and native features
|
// Handles configurable server URL and auto-redirect
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
var STORAGE_KEY = 'pedscribe_server_url';
|
var STORAGE_KEY = 'pedscribe_server_url';
|
||||||
var BIOMETRIC_KEY = 'pedscribe_biometric_enabled';
|
|
||||||
var DEFAULT_URL = 'https://app.pedshub.com';
|
var DEFAULT_URL = 'https://app.pedshub.com';
|
||||||
|
|
||||||
var setupScreen = document.getElementById('setup-screen');
|
var setupScreen = document.getElementById('setup-screen');
|
||||||
var connectingScreen = document.getElementById('connecting-screen');
|
var connectingScreen = document.getElementById('connecting-screen');
|
||||||
var biometricScreen = document.getElementById('biometric-screen');
|
|
||||||
var urlInput = document.getElementById('server-url');
|
var urlInput = document.getElementById('server-url');
|
||||||
var connectBtn = document.getElementById('btn-connect');
|
var connectBtn = document.getElementById('btn-connect');
|
||||||
var changeBtn = document.getElementById('btn-change-server');
|
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 savedUrl = localStorage.getItem(STORAGE_KEY);
|
||||||
var biometricEnabled = localStorage.getItem(BIOMETRIC_KEY) === 'true';
|
|
||||||
|
|
||||||
// ── Launch flow ──
|
|
||||||
if (savedUrl) {
|
if (savedUrl) {
|
||||||
if (biometricEnabled && isCapacitor()) {
|
showConnecting(savedUrl);
|
||||||
showBiometricScreen(savedUrl);
|
|
||||||
} else {
|
|
||||||
showConnecting(savedUrl);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
urlInput.value = DEFAULT_URL;
|
urlInput.value = DEFAULT_URL;
|
||||||
showScreen('setup');
|
showScreen('setup');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Connect button ──
|
// Connect button
|
||||||
connectBtn.addEventListener('click', function() {
|
connectBtn.addEventListener('click', function() {
|
||||||
var url = (urlInput.value || DEFAULT_URL).trim().replace(/\/+$/, '');
|
var url = (urlInput.value || DEFAULT_URL).trim().replace(/\/+$/, '');
|
||||||
if (!url.startsWith('http')) url = 'https://' + url;
|
if (!url.startsWith('http')) url = 'https://' + url;
|
||||||
|
|
@ -39,27 +29,15 @@
|
||||||
connectBtn.textContent = 'Connecting...';
|
connectBtn.textContent = 'Connecting...';
|
||||||
haptic();
|
haptic();
|
||||||
|
|
||||||
testServer(url, function(ok) {
|
localStorage.setItem(STORAGE_KEY, url);
|
||||||
if (ok) {
|
navigateToServer(url);
|
||||||
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.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
urlInput.addEventListener('keydown', function(e) {
|
urlInput.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Enter') connectBtn.click();
|
if (e.key === 'Enter') connectBtn.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Change server ──
|
// Change server
|
||||||
changeBtn.addEventListener('click', function() {
|
changeBtn.addEventListener('click', function() {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
urlInput.value = savedUrl || DEFAULT_URL;
|
urlInput.value = savedUrl || DEFAULT_URL;
|
||||||
|
|
@ -67,114 +45,21 @@
|
||||||
urlInput.focus();
|
urlInput.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Biometric unlock ──
|
// Screen management
|
||||||
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 ──
|
|
||||||
function showScreen(which) {
|
function showScreen(which) {
|
||||||
setupScreen.style.display = which === 'setup' ? '' : 'none';
|
setupScreen.style.display = which === 'setup' ? '' : 'none';
|
||||||
connectingScreen.style.display = which === 'connecting' ? '' : '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) {
|
function showConnecting(url) {
|
||||||
showScreen('connecting');
|
showScreen('connecting');
|
||||||
setTimeout(function() {
|
setTimeout(function() { navigateToServer(url); }, 800);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateToServer(url) {
|
function navigateToServer(url) {
|
||||||
window.location.href = 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() {
|
function haptic() {
|
||||||
try {
|
try {
|
||||||
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) {
|
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) {
|
||||||
|
|
@ -182,16 +67,4 @@
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} 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);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -23,22 +23,6 @@
|
||||||
<button id="btn-change-server" class="btn-link">Change Server</button>
|
<button id="btn-change-server" class="btn-link">Change Server</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Biometric lock screen -->
|
|
||||||
<div id="biometric-screen" style="display:none;">
|
|
||||||
<div class="logo-icon">
|
|
||||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="24" cy="24" r="22" fill="white" fill-opacity="0.15"/>
|
|
||||||
<path d="M24 12c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4 4s4-1.8 4-4V16c0-2.2-1.8-4-4-4z" fill="white"/>
|
|
||||||
<path d="M32 22v2c0 4.4-3.6 8-8 8s-8-3.6-8-8v-2h-2v2c0 5.1 3.8 9.3 8.7 9.9V36H20v2h8v-2h-2.7v-2.1c4.9-.6 8.7-4.8 8.7-9.9v-2h-2z" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h1>PedScribe</h1>
|
|
||||||
<p class="subtitle">Unlock to continue</p>
|
|
||||||
<button id="btn-unlock" class="btn-primary" style="margin-top:20px;">
|
|
||||||
Unlock with Biometrics
|
|
||||||
</button>
|
|
||||||
<button id="btn-skip-biometric" class="btn-link">Skip</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Server URL setup screen -->
|
<!-- Server URL setup screen -->
|
||||||
<div id="setup-screen">
|
<div id="setup-screen">
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,26 @@
|
||||||
// PedScribe Mobile Launcher
|
// PedScribe Mobile Launcher
|
||||||
// Handles server URL config, biometric auth, and native features
|
// Handles configurable server URL and auto-redirect
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
var STORAGE_KEY = 'pedscribe_server_url';
|
var STORAGE_KEY = 'pedscribe_server_url';
|
||||||
var BIOMETRIC_KEY = 'pedscribe_biometric_enabled';
|
|
||||||
var DEFAULT_URL = 'https://app.pedshub.com';
|
var DEFAULT_URL = 'https://app.pedshub.com';
|
||||||
|
|
||||||
var setupScreen = document.getElementById('setup-screen');
|
var setupScreen = document.getElementById('setup-screen');
|
||||||
var connectingScreen = document.getElementById('connecting-screen');
|
var connectingScreen = document.getElementById('connecting-screen');
|
||||||
var biometricScreen = document.getElementById('biometric-screen');
|
|
||||||
var urlInput = document.getElementById('server-url');
|
var urlInput = document.getElementById('server-url');
|
||||||
var connectBtn = document.getElementById('btn-connect');
|
var connectBtn = document.getElementById('btn-connect');
|
||||||
var changeBtn = document.getElementById('btn-change-server');
|
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 savedUrl = localStorage.getItem(STORAGE_KEY);
|
||||||
var biometricEnabled = localStorage.getItem(BIOMETRIC_KEY) === 'true';
|
|
||||||
|
|
||||||
// ── Launch flow ──
|
|
||||||
if (savedUrl) {
|
if (savedUrl) {
|
||||||
if (biometricEnabled && isCapacitor()) {
|
showConnecting(savedUrl);
|
||||||
showBiometricScreen(savedUrl);
|
|
||||||
} else {
|
|
||||||
showConnecting(savedUrl);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
urlInput.value = DEFAULT_URL;
|
urlInput.value = DEFAULT_URL;
|
||||||
showScreen('setup');
|
showScreen('setup');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Connect button ──
|
// Connect button
|
||||||
connectBtn.addEventListener('click', function() {
|
connectBtn.addEventListener('click', function() {
|
||||||
var url = (urlInput.value || DEFAULT_URL).trim().replace(/\/+$/, '');
|
var url = (urlInput.value || DEFAULT_URL).trim().replace(/\/+$/, '');
|
||||||
if (!url.startsWith('http')) url = 'https://' + url;
|
if (!url.startsWith('http')) url = 'https://' + url;
|
||||||
|
|
@ -39,27 +29,15 @@
|
||||||
connectBtn.textContent = 'Connecting...';
|
connectBtn.textContent = 'Connecting...';
|
||||||
haptic();
|
haptic();
|
||||||
|
|
||||||
testServer(url, function(ok) {
|
localStorage.setItem(STORAGE_KEY, url);
|
||||||
if (ok) {
|
navigateToServer(url);
|
||||||
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.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
urlInput.addEventListener('keydown', function(e) {
|
urlInput.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Enter') connectBtn.click();
|
if (e.key === 'Enter') connectBtn.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Change server ──
|
// Change server
|
||||||
changeBtn.addEventListener('click', function() {
|
changeBtn.addEventListener('click', function() {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
urlInput.value = savedUrl || DEFAULT_URL;
|
urlInput.value = savedUrl || DEFAULT_URL;
|
||||||
|
|
@ -67,114 +45,21 @@
|
||||||
urlInput.focus();
|
urlInput.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Biometric unlock ──
|
// Screen management
|
||||||
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 ──
|
|
||||||
function showScreen(which) {
|
function showScreen(which) {
|
||||||
setupScreen.style.display = which === 'setup' ? '' : 'none';
|
setupScreen.style.display = which === 'setup' ? '' : 'none';
|
||||||
connectingScreen.style.display = which === 'connecting' ? '' : '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) {
|
function showConnecting(url) {
|
||||||
showScreen('connecting');
|
showScreen('connecting');
|
||||||
setTimeout(function() {
|
setTimeout(function() { navigateToServer(url); }, 800);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateToServer(url) {
|
function navigateToServer(url) {
|
||||||
window.location.href = 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() {
|
function haptic() {
|
||||||
try {
|
try {
|
||||||
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) {
|
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) {
|
||||||
|
|
@ -182,16 +67,4 @@
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} 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);
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue