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>
This commit is contained in:
130
apps/mobile/src/modules/auth/login.tsx
Normal file
130
apps/mobile/src/modules/auth/login.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user