auth: replace password login with OIDC PKCE flow (Phase 5)

- Install expo-auth-session + expo-web-browser
- Add 'bincio-rec' URL scheme to app.json for deep-link redirect
- auth.ts: generate PKCE verifier/challenge, open bincio.org/oauth2/authorize
  in browser, exchange auth code for RS256 id_token, store in AsyncStorage
- SettingsScreen: remove handle/password fields, single 'Sign in with bincio'
  button that opens the browser flow
This commit is contained in:
Davide Scaini
2026-06-03 16:12:15 +02:00
parent 358f3f12c1
commit 27e7f008f0
5 changed files with 304 additions and 80 deletions
+8 -33
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import {
View, Text, TextInput, StyleSheet, Switch,
View, Text, StyleSheet, Switch,
TouchableOpacity, Alert, ScrollView, ActivityIndicator,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
@@ -131,34 +131,28 @@ function AppTab() {
function SyncTab() {
const { accent } = useTheme();
const [instanceUrl, setInstanceUrl] = useState('');
const [handle, setHandle] = useState('');
const [password, setPassword] = useState('');
const [connectedAs, setConnectedAs] = useState<string | null>(null);
const [connecting, setConnecting] = useState(false);
useEffect(() => {
loadAuthState().then((auth) => {
if (auth) { setInstanceUrl(auth.instanceUrl); setHandle(auth.handle); setConnectedAs(auth.handle); }
if (auth) setConnectedAs(auth.handle);
});
}, []);
async function handleConnect() {
if (!instanceUrl.trim()) { Alert.alert('Required', 'Enter the instance URL.'); return; }
if (!handle.trim()) { Alert.alert('Required', 'Enter your handle.'); return; }
if (!password) { Alert.alert('Required', 'Enter your password.'); return; }
setConnecting(true);
const result = await login(instanceUrl.trim(), handle.trim(), password);
const result = await login();
setConnecting(false);
if (result.ok) { setConnectedAs(result.displayName || handle.trim()); setPassword(''); }
else Alert.alert('Login failed', result.error ?? 'Unknown error');
if (result.ok) setConnectedAs(result.displayName ?? '');
else if (result.error !== 'Cancelled') Alert.alert('Sign in failed', result.error ?? 'Unknown error');
}
async function handleDisconnect() {
Alert.alert('Disconnect', 'Remove saved credentials?', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Disconnect', style: 'destructive', onPress: async () => {
await logout(); setConnectedAs(null); setHandle(''); setPassword('');
await logout(); setConnectedAs(null);
}},
]);
}
@@ -167,18 +161,6 @@ function SyncTab() {
<ScrollView contentContainerStyle={styles.content}>
<Text style={styles.sectionTitle}>bincio instance</Text>
<Text style={styles.label}>Instance URL</Text>
<TextInput
style={styles.input}
value={instanceUrl}
onChangeText={setInstanceUrl}
placeholder="https://bincio.example.com"
placeholderTextColor={colors.placeholder}
autoCapitalize="none"
keyboardType="url"
editable={!connectedAs}
/>
{connectedAs ? (
<View style={styles.connectedBox}>
<View>
@@ -191,16 +173,9 @@ function SyncTab() {
</View>
) : (
<>
<Text style={styles.label}>Handle</Text>
<TextInput style={styles.input} value={handle} onChangeText={setHandle} placeholder="your-handle" placeholderTextColor={colors.placeholder} autoCapitalize="none" autoCorrect={false} />
<Text style={styles.label}>Password</Text>
<TextInput style={styles.input} value={password} onChangeText={setPassword} placeholder="••••••••" placeholderTextColor={colors.placeholder} secureTextEntry />
<Text style={styles.hint}>Your password is used once to obtain a session token, then forgotten.</Text>
<Text style={styles.hint}>Sign in with your bincio account to sync recordings.</Text>
<TouchableOpacity style={[styles.connectBtn, connecting && styles.connectBtnDisabled]} onPress={handleConnect} disabled={connecting}>
{connecting ? <ActivityIndicator color={colors.text} /> : <Text style={styles.connectBtnText}>Connect</Text>}
{connecting ? <ActivityIndicator color={colors.text} /> : <Text style={styles.connectBtnText}>Sign in with bincio</Text>}
</TouchableOpacity>
</>
)}