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>
131 lines
3.7 KiB
TypeScript
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} />
|
|
</>
|
|
);
|
|
};
|