Journal server (Phase 1.4 + 1.5): - Add oauth_clients, oauth_codes, oauth_tokens tables to journal schema - Implement GET /oauth/authorize with PKCE flow and login redirect - Implement POST /oauth/token (authorization_code + refresh_token grants) - Add validateBearerToken() + getAuthenticatedUser() middleware - Seed trails-cool-mobile as trusted OAuth client on server startup - Add GET /.well-known/trails-cool discovery endpoint - Add returnTo support to login page and magic link verify - Add @trails-cool/api workspace dependency to journal Mobile app (Phase 1.5 + 1.6): - Login screen with server URL input and discovery validation - OAuth2 PKCE login via expo-web-browser with expo-crypto for Hermes - Token storage in expo-secure-store with auto-refresh on 401 - API client with bearer token injection and typed errors - Server URL persistence with localhost default in dev mode - API version compatibility check on app foreground - Log out + switch server on Profile tab - iOS ATS exception for local networking Tests: - PKCE crypto verification, OAuthError, token generation - Discovery endpoint response shape - API version semver compatibility - API client error types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
70 lines
2 KiB
TypeScript
70 lines
2 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { View, Text, StyleSheet, TouchableOpacity, Alert } from "react-native";
|
|
import { router } from "expo-router";
|
|
import { API_VERSION } from "@trails-cool/api";
|
|
import { logout } from "../../lib/auth";
|
|
import { getServerUrl, clearServerUrl } from "../../lib/server-config";
|
|
|
|
export default function ProfileScreen() {
|
|
const [serverUrl, setServerUrl] = useState("");
|
|
|
|
useEffect(() => {
|
|
getServerUrl().then(setServerUrl);
|
|
}, []);
|
|
|
|
const handleSwitchServer = () => {
|
|
Alert.alert(
|
|
"Switch Server",
|
|
"This will log you out and clear local data. Continue?",
|
|
[
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Switch",
|
|
style: "destructive",
|
|
onPress: async () => {
|
|
await logout();
|
|
await clearServerUrl();
|
|
router.replace("/login");
|
|
},
|
|
},
|
|
],
|
|
);
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
await logout();
|
|
router.replace("/login");
|
|
};
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.text}>Profile</Text>
|
|
<Text style={styles.server}>{serverUrl}</Text>
|
|
<Text style={styles.version}>API v{API_VERSION}</Text>
|
|
|
|
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
|
|
<Text style={styles.logoutText}>Log Out</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity onPress={handleSwitchServer}>
|
|
<Text style={styles.link}>Switch Server</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, justifyContent: "center", alignItems: "center", padding: 24 },
|
|
text: { fontSize: 18, color: "#666" },
|
|
server: { fontSize: 14, color: "#999", marginTop: 4 },
|
|
version: { fontSize: 12, color: "#999", marginTop: 4 },
|
|
logoutButton: {
|
|
marginTop: 32,
|
|
backgroundColor: "#e5e5e5",
|
|
borderRadius: 8,
|
|
paddingVertical: 12,
|
|
paddingHorizontal: 32,
|
|
},
|
|
logoutText: { fontSize: 16, color: "#333" },
|
|
link: { color: "#4A6B40", fontSize: 14, marginTop: 16 },
|
|
});
|