Add Authentik sign-in, fix tab scroll insets, and polish multi-account auth.

Mobile app detects SSO per server, supports OAuth sign-in, and preserves saved
sessions when adding accounts. Tab screens get proper chrome layout and tab-bar
clearance with scrollable page headers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-18 02:27:31 -04:00
parent 3daf123399
commit 0b2d65a4e9
21 changed files with 449 additions and 200 deletions
+32 -18
View File
@@ -50,6 +50,7 @@ export function AppLockProvider({ children }: { children: ReactNode }) {
const [biometricAvailable, setBiometricAvailable] = useState(false);
const [biometricLabel, setBiometricLabel] = useState("Biometrics");
const wasBackgrounded = useRef(false);
const biometricUnlockInProgress = useRef(false);
const hydrated = useRef(false);
useEffect(() => {
@@ -105,11 +106,17 @@ export function AppLockProvider({ children }: { children: ReactNode }) {
const subscription = AppState.addEventListener("change", (nextState: AppStateStatus) => {
if (!hydrated.current || !enabled || !activeAccountId) return;
if (nextState === "background" || nextState === "inactive") {
// Only true backgrounding should re-lock — `inactive` fires during Face ID,
// Control Center, and other system sheets and must not trigger another lock.
if (nextState === "background") {
wasBackgrounded.current = true;
}
if (nextState === "active" && wasBackgrounded.current) {
if (
nextState === "active" &&
wasBackgrounded.current &&
!biometricUnlockInProgress.current
) {
wasBackgrounded.current = false;
setIsLocked(true);
}
@@ -125,6 +132,7 @@ export function AppLockProvider({ children }: { children: ReactNode }) {
if (!stored || stored !== pin) {
return false;
}
wasBackgrounded.current = false;
setIsLocked(false);
return true;
},
@@ -136,24 +144,30 @@ export function AppLockProvider({ children }: { children: ReactNode }) {
return false;
}
const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Unlock beenvoice",
cancelLabel: "Use PIN",
disableDeviceFallback: false,
biometricsSecurityLevel: "weak",
});
biometricUnlockInProgress.current = true;
try {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: "Unlock beenvoice",
cancelLabel: "Use PIN",
disableDeviceFallback: false,
biometricsSecurityLevel: "weak",
});
if (!result.success) {
return false;
if (!result.success) {
return false;
}
if (!biometricEnabled) {
await setBiometricEnabled(activeAccountId, true);
setBiometricEnabledState(true);
}
wasBackgrounded.current = false;
setIsLocked(false);
return true;
} finally {
biometricUnlockInProgress.current = false;
}
if (!biometricEnabled) {
await setBiometricEnabled(activeAccountId, true);
setBiometricEnabledState(true);
}
setIsLocked(false);
return true;
}, [biometricAvailable, biometricEnabled, activeAccountId]);
const enableLock = useCallback(
+16 -11
View File
@@ -1,5 +1,6 @@
import { expoClient } from "@better-auth/expo/client";
import { createAuthClient } from "better-auth/react";
import { genericOAuthClient } from "better-auth/client/plugins";
import * as SecureStore from "expo-secure-store";
import {
createContext,
@@ -10,6 +11,20 @@ import {
type AuthClient = ReturnType<typeof createAuthClient>;
function createAppAuthClient(apiUrl: string, storagePrefix: string): AuthClient {
return createAuthClient({
baseURL: apiUrl,
plugins: [
expoClient({
scheme: "beenvoice",
storagePrefix,
storage: SecureStore,
}),
genericOAuthClient(),
],
});
}
const AuthContext = createContext<AuthClient | null>(null);
export function AuthProvider({
@@ -22,17 +37,7 @@ export function AuthProvider({
children: ReactNode;
}) {
const client = useMemo(
() =>
createAuthClient({
baseURL: apiUrl,
plugins: [
expoClient({
scheme: "beenvoice",
storagePrefix,
storage: SecureStore,
}),
],
}),
() => createAppAuthClient(apiUrl, storagePrefix),
[apiUrl, storagePrefix],
);