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": {
|
||||
"allowMixedContent": true,
|
||||
"backgroundColor": "#2563eb"
|
||||
"backgroundColor": "#2563eb",
|
||||
"webContentsDebuggingEnabled": true,
|
||||
"appendUserAgent": "PedScribe-Android"
|
||||
},
|
||||
"ios": {
|
||||
"backgroundColor": "#2563eb",
|
||||
|
|
|
|||
|
|
@ -23,22 +23,6 @@
|
|||
<button id="btn-change-server" class="btn-link">Change Server</button>
|
||||
</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 -->
|
||||
<div id="setup-screen">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@
|
|||
},
|
||||
"android": {
|
||||
"allowMixedContent": true,
|
||||
"backgroundColor": "#2563eb"
|
||||
"backgroundColor": "#2563eb",
|
||||
"webContentsDebuggingEnabled": true,
|
||||
"appendUserAgent": "PedScribe-Android"
|
||||
},
|
||||
"ios": {
|
||||
"backgroundColor": "#2563eb",
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@
|
|||
},
|
||||
"android": {
|
||||
"allowMixedContent": true,
|
||||
"backgroundColor": "#2563eb"
|
||||
"backgroundColor": "#2563eb",
|
||||
"webContentsDebuggingEnabled": true,
|
||||
"appendUserAgent": "PedScribe-Android"
|
||||
},
|
||||
"ios": {
|
||||
"backgroundColor": "#2563eb",
|
||||
|
|
|
|||
|
|
@ -23,22 +23,6 @@
|
|||
<button id="btn-change-server" class="btn-link">Change Server</button>
|
||||
</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 -->
|
||||
<div id="setup-screen">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -23,22 +23,6 @@
|
|||
<button id="btn-change-server" class="btn-link">Change Server</button>
|
||||
</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 -->
|
||||
<div id="setup-screen">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in a new issue