feat: replace password login with OIDC PKCE flow

This commit is contained in:
Davide Scaini
2026-06-03 16:21:28 +02:00
parent 78af14a78d
commit 6e69358e1e
5 changed files with 179 additions and 110 deletions
+29 -103
View File
@@ -1,17 +1,17 @@
import { useSQLiteContext } from 'expo-sqlite';
import { useState } from 'react';
import {
ActivityIndicator, Platform, Pressable, ScrollView, StyleSheet,
ActivityIndicator, Alert, Platform, Pressable, ScrollView, StyleSheet,
Text, TextInput, View,
} from 'react-native';
import { deleteRemoteActivities, getSetting, setSetting, useSetting } from '@/db/queries';
import { deleteRemoteActivities, setSetting, useSetting } from '@/db/queries';
import { PALETTES, type PaletteKey } from '@/theme';
import { useTheme, usePaletteControl } from '@/ThemeContext';
import { login, logout } from '@/services/auth';
export default function SettingsScreen() {
const db = useSQLiteContext();
const storedUrl = useSetting('instance_url') ?? '';
const storedHandle = useSetting('handle') ?? '';
const storedPath = useSetting('auto_import_path') ?? '';
const storedToken = useSetting('api_token');
@@ -19,66 +19,39 @@ export default function SettingsScreen() {
const storedSyncUpload = useSetting('sync_upload') === 'true';
const storedUploadFormat = (useSetting('upload_format') ?? 'raw') as 'raw' | 'bas';
const [instanceUrl, setInstanceUrl] = useState(storedUrl);
const [handle, setHandle] = useState(storedHandle);
const [autoPath, setAutoPath] = useState(storedPath);
const [syncMode, setSyncMode] = useState(storedSyncMode);
const [syncUpload, setSyncUpload] = useState(storedSyncUpload);
const [uploadFormat, setUploadFormat] = useState(storedUploadFormat);
const [saved, setSaved] = useState(false);
const theme = useTheme();
const { paletteKey: palette, setPaletteOverride } = usePaletteControl();
const [password, setPassword] = useState('');
const [connecting, setConnecting] = useState(false);
const [connectMsg, setConnectMsg] = useState<{ ok: boolean; text: string } | null>(null);
const [resetArmed, setResetArmed] = useState(false);
const [resetMsg, setResetMsg] = useState<string | null>(null);
async function save() {
await setSetting(db, 'instance_url', instanceUrl.trim());
await setSetting(db, 'handle', handle.trim());
setSaved(true);
setTimeout(() => setSaved(false), 2000);
}
async function connect() {
const url = instanceUrl.trim().replace(/\/$/, '');
const h = handle.trim();
if (!url || !h || !password) {
setConnectMsg({ ok: false, text: 'Fill in URL, handle, and password first.' });
return;
}
async function handleConnect() {
setConnecting(true);
setConnectMsg(null);
try {
const resp = await fetch(`${url}/api/auth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ handle: h, password }),
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
setConnectMsg({ ok: false, text: err.detail ?? `Error ${resp.status}` });
return;
}
const data = await resp.json();
await setSetting(db, 'instance_url', url);
await setSetting(db, 'handle', h);
await setSetting(db, 'api_token', data.token);
setPassword('');
setConnectMsg({ ok: true, text: `Connected as ${data.display_name || h}` });
} catch {
setConnectMsg({ ok: false, text: 'Could not reach instance — check the URL.' });
} finally {
setConnecting(false);
const result = await login(db);
setConnecting(false);
if (result.ok) {
setConnectMsg({ ok: true, text: `Connected as ${result.displayName ?? ''}` });
} else if (result.error !== 'Cancelled') {
setConnectMsg({ ok: false, text: result.error ?? 'Sign in failed' });
}
}
async function disconnect() {
await setSetting(db, 'api_token', '');
setConnectMsg(null);
async function handleDisconnect() {
Alert.alert('Disconnect', 'Remove saved credentials?', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Disconnect', style: 'destructive', onPress: async () => {
await logout(db);
setConnectMsg(null);
}},
]);
}
async function resetSyncedData() {
@@ -98,78 +71,36 @@ export default function SettingsScreen() {
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.header}>Settings</Text>
<Section title="Instance">
<Field
label="Instance URL"
placeholder="https://bincio.org"
value={instanceUrl}
onChangeText={setInstanceUrl}
autoCapitalize="none"
keyboardType="url"
/>
<Field
label="Handle"
placeholder="yourhandle"
value={handle}
onChangeText={setHandle}
autoCapitalize="none"
/>
<Text style={styles.hint}>
Connect to a Bincio instance to sync your activities. Leave blank to use
the app offline only.
</Text>
</Section>
<Pressable style={styles.saveButton} onPress={save}>
<Text style={styles.saveButtonText}>
{saved ? '✓ Saved' : 'Save'}
</Text>
</Pressable>
<Section title="Connection">
{isConnected ? (
<>
<Row label="Status" value={`Connected as ${storedHandle || '—'}`} />
<Pressable style={styles.disconnectButton} onPress={disconnect}>
<Pressable style={styles.disconnectButton} onPress={handleDisconnect}>
<Text style={styles.disconnectText}>Disconnect</Text>
</Pressable>
</>
) : (
<>
<Field
label="Password"
placeholder="••••••••"
value={password}
onChangeText={setPassword}
autoCapitalize="none"
secureTextEntry
/>
<Pressable
style={[styles.connectButton, connecting && styles.buttonDisabled]}
onPress={connecting ? undefined : connect}
>
{connecting
? <ActivityIndicator color="#fff" size="small" />
: <Text style={styles.connectText}>Connect</Text>}
</Pressable>
</>
<Pressable
style={[styles.connectButton, connecting && styles.buttonDisabled]}
onPress={connecting ? undefined : handleConnect}
>
{connecting
? <ActivityIndicator color="#fff" size="small" />
: <Text style={styles.connectText}>Sign in with bincio</Text>}
</Pressable>
)}
{connectMsg && (
<Text style={connectMsg.ok ? styles.msgOk : styles.msgErr}>
{connectMsg.text}
</Text>
)}
<Text style={styles.hint}>
Your password is used once to obtain a session token, then forgotten.
The token is stored locally and sent with each sync request.
</Text>
</Section>
{Platform.OS === 'android' && (
<Section title="Auto-import (Android)">
{!storedUrl ? (
{!isConnected ? (
<Text style={[styles.hint, styles.hintWarn]}>
Configure and save a Bincio instance URL above first it's needed to download the extraction engine.
Sign in to a Bincio instance above first it's needed to download the extraction engine.
</Text>
) : (
<>
@@ -356,11 +287,6 @@ const styles = StyleSheet.create({
},
rowLabel: { color: '#a1a1aa', fontSize: 14 },
rowValue: { color: '#71717a', fontSize: 14 },
saveButton: {
backgroundColor: '#2563eb', borderRadius: 10,
paddingVertical: 14, alignItems: 'center', marginBottom: 28,
},
saveButtonText: { color: '#fff', fontWeight: '600', fontSize: 16 },
connectButton: {
backgroundColor: '#059669', borderRadius: 8, margin: 12,
paddingVertical: 12, alignItems: 'center',