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
co-authored by Cursor
parent 3daf123399
commit 0b2d65a4e9
21 changed files with 449 additions and 200 deletions
+12
View File
@@ -0,0 +1,12 @@
import { router } from "expo-router";
import { prepareForAdditionalSignIn } from "@/lib/auth-storage";
/** Switch to guest mode and open sign-in without wiping other saved accounts. */
export async function startAdditionalAccountSignIn(
clearActiveAccount: () => Promise<void>,
) {
await clearActiveAccount();
await prepareForAdditionalSignIn();
router.replace("/(auth)/sign-in");
}
+21
View File
@@ -0,0 +1,21 @@
export type AuthCapabilities = {
authentik: boolean;
signupsDisabled: boolean;
};
const DEFAULT_CAPABILITIES: AuthCapabilities = {
authentik: false,
signupsDisabled: false,
};
export async function fetchAuthCapabilities(apiUrl: string): Promise<AuthCapabilities> {
const base = apiUrl.replace(/\/$/, "");
try {
const response = await fetch(`${base}/api/auth/capabilities`);
if (!response.ok) return DEFAULT_CAPABILITIES;
return (await response.json()) as AuthCapabilities;
} catch {
return DEFAULT_CAPABILITIES;
}
}
+15
View File
@@ -0,0 +1,15 @@
import type { createAuthClient } from "better-auth/react";
type AuthClient = ReturnType<typeof createAuthClient>;
type OAuth2SignIn = (input: {
providerId: string;
callbackURL: string;
}) => Promise<{ error?: { message?: string } | null }>;
export async function signInWithAuthentik(authClient: AuthClient, callbackURL: string) {
return (authClient.signIn as unknown as { oauth2: OAuth2SignIn }).oauth2({
providerId: "authentik",
callbackURL,
});
}
+27
View File
@@ -41,6 +41,33 @@ export async function migrateAuthStorage(fromPrefix: string, toPrefix: string):
);
}
export async function clearAuthStorage(prefix: string): Promise<void> {
await Promise.all(
AUTH_STORAGE_SUFFIXES.map(async (suffix) => {
const key = storageKeyForPrefix(prefix, suffix);
const value = await SecureStore.getItemAsync(key);
if (value?.startsWith(CHUNK_MARKER)) {
const count = Number(value.slice(CHUNK_MARKER.length));
if (Number.isInteger(count) && count > 0) {
await Promise.all(
Array.from({ length: count }, (_, index) =>
SecureStore.deleteItemAsync(`${key}.${index}`),
),
);
}
}
await SecureStore.deleteItemAsync(key);
}),
);
}
/** Clears guest auth storage before signing into an additional account. */
export async function prepareForAdditionalSignIn(): Promise<void> {
await clearAuthStorage(GUEST_AUTH_STORAGE_PREFIX);
}
export async function finalizeAuthenticatedAccount(input: {
apiUrl: string;
userId: string;
+34
View File
@@ -0,0 +1,34 @@
import type { createAuthClient } from "better-auth/react";
import { finalizeAuthenticatedAccount } from "@/lib/auth-storage";
type AuthClient = ReturnType<typeof createAuthClient>;
export async function completeSignInAfterAuth(
authClient: AuthClient,
input: {
apiUrl: string;
activeAccountId: string | null;
registerAccount: (account: {
instanceUrl: string;
userId: string;
email: string;
name: string;
}) => Promise<unknown>;
},
): Promise<boolean> {
const session = await authClient.getSession();
const user = session.data?.user;
if (!user) return false;
await finalizeAuthenticatedAccount({
apiUrl: input.apiUrl,
userId: user.id,
email: user.email,
name: user.name,
activeAccountId: input.activeAccountId,
registerAccount: input.registerAccount,
});
return true;
}
+11
View File
@@ -50,6 +50,17 @@ export function useFloatingActionBottom(): number {
return tabBar + homeIndicator + spacing.xs;
}
/**
* Bottom padding for tab-root ScrollViews (Dashboard, Invoices, etc.).
* Uses full tab-bar clearance — do not trim; undershooting hides content under the bar.
*/
export function useTabScreenScrollPadding(): number {
const { bottom: homeIndicator } = useSafeAreaInsets();
const tabBar = useNativeTabBarHeight();
return tabBar + homeIndicator + spacing.sm;
}
/** @deprecated Use useTabBarScrollPadding */
export function useTabBarInset() {
return useTabBarScrollPadding();
+2 -1
View File
@@ -6,12 +6,13 @@ import { spacing } from "@/constants/theme";
export const tabLayout = StyleSheet.create({
pageHeader: {
gap: 4,
paddingTop: spacing.md,
paddingBottom: spacing.md,
},
scrollContent: {
paddingHorizontal: spacing.md,
},
scrollBody: {
gap: spacing.md,
marginTop: spacing.sm,
},
});