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:
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user