Files
turbostarter/apps/mobile/src/modules/auth/login.tsx
Alejandro Gutiérrez 3527e732d4 feat: turbostarter boilerplate
Production-ready Next.js boilerplate with:
- Runtime env validation (fail-fast on missing vars)
- Feature-gated config (S3, Stripe, email, OAuth)
- Docker + Coolify deployment pipeline
- PostgreSQL + pgvector, MinIO S3, Better Auth
- TypeScript strict mode (no ignoreBuildErrors)
- i18n (en/es), AI modules, billing, monitoring

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 01:01:55 +01:00

131 lines
3.7 KiB
TypeScript

import { memo, useState } from "react";
import { View } from "react-native";
import { SecondFactor } from "@turbostarter/auth";
import { useTranslation } from "@turbostarter/i18n";
import { authConfig } from "~/config/auth";
import { AnonymousLogin } from "~/modules/auth/form/anonymous";
import { LOGIN_OPTIONS } from "~/modules/auth/form/login/constants";
import { LoginForm } from "~/modules/auth/form/login/form";
import { RegisterCta } from "~/modules/auth/form/register-form";
import { SocialProviders } from "~/modules/auth/form/social-providers";
import { TwoFactorForm, TwoFactorCta } from "~/modules/auth/form/two-factor";
import { AuthLayout } from "~/modules/auth/layout/base";
import { AuthDivider } from "~/modules/auth/layout/divider";
import { AuthHeader } from "~/modules/auth/layout/header";
import { InvitationDisclaimer } from "~/modules/auth/layout/invitation-disclaimer";
import type { Route } from "expo-router";
import type { LoginOption } from "~/modules/auth/form/login/constants";
const LoginStep = {
FORM: "form",
TWO_FACTOR: "twoFactor",
} as const;
type LoginStep = (typeof LoginStep)[keyof typeof LoginStep];
interface LoginFlowProps {
readonly redirectTo?: Route;
readonly invitationId?: string;
readonly email?: string;
}
export const LoginFlow = ({
redirectTo,
invitationId,
email,
}: LoginFlowProps) => {
const [step, setStep] = useState<LoginStep>(LoginStep.FORM);
return (
<AuthLayout>
{(() => {
switch (step) {
case LoginStep.FORM:
return (
<Login
redirectTo={redirectTo}
invitationId={invitationId}
email={email}
onTwoFactorRedirect={() => setStep(LoginStep.TWO_FACTOR)}
/>
);
case LoginStep.TWO_FACTOR:
return <TwoFactor redirectTo={redirectTo} />;
}
})()}
</AuthLayout>
);
};
interface LoginProps extends LoginFlowProps {
readonly onTwoFactorRedirect?: () => void;
}
const Login = memo<LoginProps>(
({ redirectTo, invitationId, email, onTwoFactorRedirect }) => {
const { t } = useTranslation("auth");
const options = Object.entries(authConfig.providers)
.filter(
([provider, enabled]) =>
enabled && LOGIN_OPTIONS.includes(provider as LoginOption),
)
.map(([provider]) => provider as LoginOption);
return (
<>
<AuthHeader
title={t("login.header.title")}
description={t("login.header.description")}
/>
{invitationId && <InvitationDisclaimer />}
<SocialProviders
providers={authConfig.providers.oAuth}
redirectTo={redirectTo}
/>
{authConfig.providers.oAuth.length > 0 && options.length > 0 && (
<AuthDivider />
)}
<View className="gap-2">
<LoginForm
options={options}
redirectTo={redirectTo}
email={email}
onTwoFactorRedirect={onTwoFactorRedirect}
/>
{authConfig.providers.anonymous && <AnonymousLogin />}
</View>
<RegisterCta />
</>
);
},
);
const TwoFactor = ({ redirectTo }: LoginFlowProps) => {
const [factor, setFactor] = useState<SecondFactor>(SecondFactor.TOTP);
const { t } = useTranslation("auth");
const Form = TwoFactorForm[factor];
const Cta =
factor === SecondFactor.TOTP
? TwoFactorCta[SecondFactor.BACKUP_CODE]
: TwoFactorCta[SecondFactor.TOTP];
return (
<>
<AuthHeader
title={t(`login.twoFactor.${factor}.header.title`)}
description={t(`login.twoFactor.${factor}.header.description`)}
/>
<Form redirectTo={redirectTo} />
<Cta onFactorChange={setFactor} />
</>
);
};