Add Google sign in and two-step auth flow (#193)
This commit is contained in:
parent
a98407176b
commit
701ace3dff
@ -30,6 +30,8 @@
|
|||||||
# Required when AUTH_MODE=hosted
|
# Required when AUTH_MODE=hosted
|
||||||
# BETTER_AUTH_SECRET=replace-with-a-long-random-secret-at-least-32-characters
|
# BETTER_AUTH_SECRET=replace-with-a-long-random-secret-at-least-32-characters
|
||||||
# BETTER_AUTH_URL=http://localhost:3001
|
# BETTER_AUTH_URL=http://localhost:3001
|
||||||
|
# GOOGLE_CLIENT_ID=replace-with-your-google-oauth-client-id
|
||||||
|
# GOOGLE_CLIENT_SECRET=replace-with-your-google-oauth-client-secret
|
||||||
# POSTHOG_PUBLIC_KEY=replace-with-your-posthog-project-api-key
|
# POSTHOG_PUBLIC_KEY=replace-with-your-posthog-project-api-key
|
||||||
# POSTHOG_HOST=https://us.i.posthog.com
|
# POSTHOG_HOST=https://us.i.posthog.com
|
||||||
# LOOPS_API_KEY=replace-with-your-loops-api-key
|
# LOOPS_API_KEY=replace-with-your-loops-api-key
|
||||||
|
|||||||
@ -36,6 +36,68 @@ export function getFormError(error: unknown) {
|
|||||||
return getSharedFormError(error);
|
return getSharedFormError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AuthMethodChooser({
|
||||||
|
googleLabel,
|
||||||
|
emailLabel = "Continue with email",
|
||||||
|
isBusy,
|
||||||
|
disabled,
|
||||||
|
onContinueWithGoogle,
|
||||||
|
onContinueWithEmail,
|
||||||
|
}: {
|
||||||
|
googleLabel: string;
|
||||||
|
emailLabel?: string;
|
||||||
|
isBusy?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onContinueWithGoogle: () => void;
|
||||||
|
onContinueWithEmail: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn w-full bg-white text-neutral border border-base-content/15 hover:bg-base-100 hover:border-base-content/25 disabled:bg-base-300"
|
||||||
|
onClick={onContinueWithGoogle}
|
||||||
|
disabled={disabled || isBusy}
|
||||||
|
>
|
||||||
|
<GoogleLogo />
|
||||||
|
{isBusy ? "Opening Google..." : googleLabel}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-soft w-full"
|
||||||
|
onClick={onContinueWithEmail}
|
||||||
|
disabled={disabled || isBusy}
|
||||||
|
>
|
||||||
|
{emailLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GoogleLogo() {
|
||||||
|
return (
|
||||||
|
<svg aria-hidden="true" viewBox="0 0 18 18" className="size-4 shrink-0">
|
||||||
|
<path
|
||||||
|
fill="#4285F4"
|
||||||
|
d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.84.86-3.04.86-2.34 0-4.33-1.58-5.04-3.72H.94v2.34A9 9 0 0 0 9 18Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC05"
|
||||||
|
d="M3.96 10.7A5.4 5.4 0 0 1 3.68 9c0-.59.1-1.16.28-1.7V4.96H.94A9 9 0 0 0 0 9c0 1.45.34 2.82.94 4.04l3.02-2.34Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.58A8.64 8.64 0 0 0 9 0 9 9 0 0 0 .94 4.96L3.96 7.3C4.67 5.16 6.66 3.58 9 3.58Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function AuthPageCard({
|
export function AuthPageCard({
|
||||||
title,
|
title,
|
||||||
helperText,
|
helperText,
|
||||||
|
|||||||
2
src/env.d.ts
vendored
2
src/env.d.ts
vendored
@ -13,6 +13,8 @@ declare namespace Cloudflare {
|
|||||||
POSTHOG_HOST?: string;
|
POSTHOG_HOST?: string;
|
||||||
BETTER_AUTH_SECRET?: string;
|
BETTER_AUTH_SECRET?: string;
|
||||||
BETTER_AUTH_URL?: string;
|
BETTER_AUTH_URL?: string;
|
||||||
|
GOOGLE_CLIENT_ID?: string;
|
||||||
|
GOOGLE_CLIENT_SECRET?: string;
|
||||||
LOOPS_API_KEY?: string;
|
LOOPS_API_KEY?: string;
|
||||||
LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID?: string;
|
LOOPS_TRANSACTIONAL_VERIFY_EMAIL_ID?: string;
|
||||||
LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID?: string;
|
LOOPS_TRANSACTIONAL_RESET_PASSWORD_ID?: string;
|
||||||
|
|||||||
@ -74,6 +74,7 @@ function createAuth() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
socialProviders: getSocialProviders(),
|
||||||
trustedOrigins: getTrustedOrigins(baseUrl),
|
trustedOrigins: getTrustedOrigins(baseUrl),
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
provider: "sqlite",
|
provider: "sqlite",
|
||||||
@ -146,6 +147,25 @@ function getHostedSecret() {
|
|||||||
return secret;
|
return secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSocialProviders() {
|
||||||
|
const googleClientId = env.GOOGLE_CLIENT_ID?.trim();
|
||||||
|
const googleClientSecret = env.GOOGLE_CLIENT_SECRET?.trim();
|
||||||
|
|
||||||
|
if (!googleClientId || !googleClientSecret) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
google: {
|
||||||
|
clientId: googleClientId,
|
||||||
|
clientSecret: googleClientSecret,
|
||||||
|
mapProfileToUser: (profile: { name?: string }) => ({
|
||||||
|
name: profile.name,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function hasHostedAuthEmailConfig() {
|
function hasHostedAuthEmailConfig() {
|
||||||
const loopsVars = [
|
const loopsVars = [
|
||||||
"LOOPS_API_KEY",
|
"LOOPS_API_KEY",
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useState } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
AuthPageCard,
|
AuthPageCard,
|
||||||
|
AuthMethodChooser,
|
||||||
authRedirectSearchSchema,
|
authRedirectSearchSchema,
|
||||||
getFieldError,
|
getFieldError,
|
||||||
getFormError,
|
getFormError,
|
||||||
@ -34,6 +35,9 @@ function SignInPage() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [isSendingVerification, setIsSendingVerification] = useState(false);
|
const [isSendingVerification, setIsSendingVerification] = useState(false);
|
||||||
|
const [showEmailForm, setShowEmailForm] = useState(false);
|
||||||
|
const [isStartingGoogle, setIsStartingGoogle] = useState(false);
|
||||||
|
const [socialError, setSocialError] = useState<string | null>(null);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -132,19 +136,52 @@ function SignInPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleContinueWithGoogle() {
|
||||||
|
setSocialError(null);
|
||||||
|
setIsStartingGoogle(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
captureClientEvent("auth:sign_in_google_start", {
|
||||||
|
redirect_to: redirectTo,
|
||||||
|
});
|
||||||
|
const result = await authClient.signIn.social({
|
||||||
|
provider: "google",
|
||||||
|
callbackURL: authCallbackURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
setSocialError(
|
||||||
|
result.error.message || "Google sign in is not available right now.",
|
||||||
|
);
|
||||||
|
setIsStartingGoogle(false);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setSocialError("Google sign in is not available right now.");
|
||||||
|
setIsStartingGoogle(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthPageCard
|
<AuthPageCard
|
||||||
title="Sign in"
|
title="Sign in"
|
||||||
footer={
|
footer={
|
||||||
isHostedMode ? (
|
isHostedMode ? (
|
||||||
<div className="flex justify-between text-sm text-base-content/50">
|
<div
|
||||||
<Link
|
className={
|
||||||
to="/forgot-password"
|
showEmailForm
|
||||||
search={getSignInSearch(redirectTo)}
|
? "flex justify-between text-sm text-base-content/50"
|
||||||
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
|
: "text-sm text-base-content/50"
|
||||||
>
|
}
|
||||||
Forgot password?
|
>
|
||||||
</Link>
|
{showEmailForm ? (
|
||||||
|
<Link
|
||||||
|
to="/forgot-password"
|
||||||
|
search={getSignInSearch(redirectTo)}
|
||||||
|
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
<Link
|
<Link
|
||||||
to="/sign-up"
|
to="/sign-up"
|
||||||
search={getSignInSearch(redirectTo)}
|
search={getSignInSearch(redirectTo)}
|
||||||
@ -156,108 +193,128 @@ function SignInPage() {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<form
|
{!showEmailForm ? (
|
||||||
className="space-y-4"
|
<>
|
||||||
onSubmit={(event) => {
|
<AuthMethodChooser
|
||||||
event.preventDefault();
|
googleLabel="Continue with Google"
|
||||||
void form.handleSubmit();
|
disabled={!isHostedMode}
|
||||||
}}
|
isBusy={isStartingGoogle}
|
||||||
>
|
onContinueWithGoogle={() => {
|
||||||
<form.Field name="email">
|
void handleContinueWithGoogle();
|
||||||
{(field) => {
|
}}
|
||||||
const error = getFieldError(field.state.meta.errors);
|
onContinueWithEmail={() => {
|
||||||
|
setShowEmailForm(true);
|
||||||
return (
|
setSocialError(null);
|
||||||
<div>
|
}}
|
||||||
<input
|
/>
|
||||||
type="email"
|
{socialError ? (
|
||||||
className="input input-bordered w-full"
|
<p className="text-sm text-error">{socialError}</p>
|
||||||
placeholder="Email address..."
|
) : null}
|
||||||
value={field.state.value}
|
</>
|
||||||
onChange={(event) => field.handleChange(event.target.value)}
|
) : (
|
||||||
autoComplete="email"
|
<form
|
||||||
disabled={!isHostedMode}
|
className="space-y-4"
|
||||||
required
|
onSubmit={(event) => {
|
||||||
/>
|
event.preventDefault();
|
||||||
{error ? (
|
void form.handleSubmit();
|
||||||
<p className="mt-1 text-sm text-error">{error}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<form.Field name="password">
|
|
||||||
{(field) => {
|
|
||||||
const error = getFieldError(field.state.meta.errors);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className="input input-bordered w-full"
|
|
||||||
placeholder="Password..."
|
|
||||||
value={field.state.value}
|
|
||||||
onChange={(event) => field.handleChange(event.target.value)}
|
|
||||||
autoComplete="current-password"
|
|
||||||
disabled={!isHostedMode}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
{error ? (
|
|
||||||
<p className="mt-1 text-sm text-error">{error}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
{verificationEmail ? (
|
|
||||||
<div className="alert alert-warning items-start">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className="text-sm">
|
|
||||||
Please check {verificationEmail} for a link to confirm your
|
|
||||||
email.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm btn-outline"
|
|
||||||
onClick={() => {
|
|
||||||
void handleResendVerification();
|
|
||||||
}}
|
|
||||||
disabled={isSendingVerification}
|
|
||||||
>
|
|
||||||
{isSendingVerification
|
|
||||||
? "Sending email..."
|
|
||||||
: "Send another email"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<form.Subscribe
|
|
||||||
selector={(state) => ({
|
|
||||||
submitError: state.errorMap.onSubmit,
|
|
||||||
isSubmitting: state.isSubmitting,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{({ submitError, isSubmitting }) => {
|
<form.Field name="email">
|
||||||
const errorMessage = getFormError(submitError);
|
{(field) => {
|
||||||
return (
|
const error = getFieldError(field.state.meta.errors);
|
||||||
<>
|
|
||||||
{errorMessage ? (
|
return (
|
||||||
<p className="text-sm text-error">{errorMessage}</p>
|
<div>
|
||||||
) : null}
|
<input
|
||||||
|
type="email"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
placeholder="Email address..."
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
|
autoComplete="email"
|
||||||
|
disabled={!isHostedMode}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{error ? (
|
||||||
|
<p className="mt-1 text-sm text-error">{error}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="password">
|
||||||
|
{(field) => {
|
||||||
|
const error = getFieldError(field.state.meta.errors);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
placeholder="Password..."
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
|
autoComplete="current-password"
|
||||||
|
disabled={!isHostedMode}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{error ? (
|
||||||
|
<p className="mt-1 text-sm text-error">{error}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
{verificationEmail ? (
|
||||||
|
<div className="alert alert-warning items-start">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm">
|
||||||
|
Please check {verificationEmail} for a link to confirm your
|
||||||
|
email.
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
className="btn btn-soft w-full"
|
type="button"
|
||||||
disabled={!isHostedMode || isSubmitting}
|
className="btn btn-sm btn-outline"
|
||||||
|
onClick={() => {
|
||||||
|
void handleResendVerification();
|
||||||
|
}}
|
||||||
|
disabled={isSendingVerification}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Signing in..." : "Sign in"}
|
{isSendingVerification
|
||||||
|
? "Sending email..."
|
||||||
|
: "Send another email"}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</div>
|
||||||
);
|
</div>
|
||||||
}}
|
) : null}
|
||||||
</form.Subscribe>
|
|
||||||
</form>
|
<form.Subscribe
|
||||||
|
selector={(state) => ({
|
||||||
|
submitError: state.errorMap.onSubmit,
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{({ submitError, isSubmitting }) => {
|
||||||
|
const errorMessage = getFormError(submitError);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{errorMessage ? (
|
||||||
|
<p className="text-sm text-error">{errorMessage}</p>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
className="btn btn-soft w-full"
|
||||||
|
disabled={!isHostedMode || isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Signing in..." : "Sign in"}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</form.Subscribe>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</AuthPageCard>
|
</AuthPageCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { useForm } from "@tanstack/react-form";
|
import { useForm } from "@tanstack/react-form";
|
||||||
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
AuthPageCard,
|
AuthPageCard,
|
||||||
|
AuthMethodChooser,
|
||||||
authRedirectSearchSchema,
|
authRedirectSearchSchema,
|
||||||
getFieldError,
|
getFieldError,
|
||||||
getFormError,
|
getFormError,
|
||||||
@ -46,6 +48,9 @@ function SignUpPage() {
|
|||||||
const search = Route.useSearch();
|
const search = Route.useSearch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { redirectTo, isHostedMode } = useAuthPageState(search.redirect);
|
const { redirectTo, isHostedMode } = useAuthPageState(search.redirect);
|
||||||
|
const [showEmailForm, setShowEmailForm] = useState(false);
|
||||||
|
const [isStartingGoogle, setIsStartingGoogle] = useState(false);
|
||||||
|
const [socialError, setSocialError] = useState<string | null>(null);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -104,178 +109,239 @@ function SignUpPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function handleContinueWithGoogle() {
|
||||||
|
const callbackURL = redirectTo === "/" ? "/subscribe" : redirectTo;
|
||||||
|
setSocialError(null);
|
||||||
|
setIsStartingGoogle(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
captureClientEvent("auth:sign_up_google_start", {
|
||||||
|
redirect_to: callbackURL,
|
||||||
|
});
|
||||||
|
const result = await authClient.signIn.social({
|
||||||
|
provider: "google",
|
||||||
|
callbackURL,
|
||||||
|
newUserCallbackURL: callbackURL,
|
||||||
|
requestSignUp: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
setSocialError(
|
||||||
|
result.error.message || "Google sign up is not available right now.",
|
||||||
|
);
|
||||||
|
setIsStartingGoogle(false);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setSocialError("Google sign up is not available right now.");
|
||||||
|
setIsStartingGoogle(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthPageCard
|
<AuthPageCard
|
||||||
title="Create your account"
|
title="Create your account"
|
||||||
footer={
|
footer={
|
||||||
isHostedMode ? (
|
isHostedMode ? (
|
||||||
<div className="space-y-4">
|
showEmailForm ? (
|
||||||
<p className="text-sm leading-relaxed text-base-content/60">
|
<button
|
||||||
By signing up, you agree to our{" "}
|
type="button"
|
||||||
<a
|
className="text-sm text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
|
||||||
href="https://openseo.so/terms-and-conditions"
|
onClick={() => {
|
||||||
target="_blank"
|
setShowEmailForm(false);
|
||||||
rel="noreferrer"
|
setSocialError(null);
|
||||||
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
|
}}
|
||||||
>
|
>
|
||||||
Terms
|
Back to signup
|
||||||
</a>{" "}
|
</button>
|
||||||
and{" "}
|
) : (
|
||||||
<a
|
<div className="space-y-4">
|
||||||
href="https://openseo.so/privacy"
|
<p className="text-sm leading-relaxed text-base-content/60">
|
||||||
target="_blank"
|
By signing up, you agree to our{" "}
|
||||||
rel="noreferrer"
|
<a
|
||||||
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
|
href="https://openseo.so/terms-and-conditions"
|
||||||
>
|
target="_blank"
|
||||||
Privacy Policy
|
rel="noreferrer"
|
||||||
</a>
|
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
|
||||||
.
|
>
|
||||||
</p>
|
Terms
|
||||||
|
</a>{" "}
|
||||||
|
and{" "}
|
||||||
|
<a
|
||||||
|
href="https://openseo.so/privacy"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
|
||||||
|
>
|
||||||
|
Privacy Policy
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p className="text-sm text-base-content/50">
|
<p className="text-sm text-base-content/50">
|
||||||
Already have an account?{" "}
|
Already have an account?{" "}
|
||||||
<Link
|
<Link
|
||||||
to="/sign-in"
|
to="/sign-in"
|
||||||
search={getSignInSearch(redirectTo)}
|
search={getSignInSearch(redirectTo)}
|
||||||
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
|
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<form
|
{!showEmailForm ? (
|
||||||
className="space-y-4"
|
<>
|
||||||
onSubmit={(event) => {
|
<AuthMethodChooser
|
||||||
event.preventDefault();
|
googleLabel="Continue with Google"
|
||||||
void form.handleSubmit();
|
disabled={!isHostedMode}
|
||||||
}}
|
isBusy={isStartingGoogle}
|
||||||
>
|
onContinueWithGoogle={() => {
|
||||||
<form.Field name="name">
|
void handleContinueWithGoogle();
|
||||||
{(field) => {
|
}}
|
||||||
const error = getFieldError(field.state.meta.errors);
|
onContinueWithEmail={() => {
|
||||||
|
setShowEmailForm(true);
|
||||||
return (
|
setSocialError(null);
|
||||||
<div>
|
}}
|
||||||
<input
|
/>
|
||||||
type="text"
|
{socialError ? (
|
||||||
className="input input-bordered w-full"
|
<p className="text-sm text-error">{socialError}</p>
|
||||||
placeholder="Name (optional)..."
|
) : null}
|
||||||
value={field.state.value}
|
</>
|
||||||
onChange={(event) => field.handleChange(event.target.value)}
|
) : (
|
||||||
autoComplete="name"
|
<form
|
||||||
disabled={!isHostedMode}
|
className="space-y-4"
|
||||||
/>
|
onSubmit={(event) => {
|
||||||
{error ? (
|
event.preventDefault();
|
||||||
<p className="mt-1 text-sm text-error">{error}</p>
|
void form.handleSubmit();
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<form.Field name="email">
|
|
||||||
{(field) => {
|
|
||||||
const error = getFieldError(field.state.meta.errors);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
className="input input-bordered w-full"
|
|
||||||
placeholder="Email address..."
|
|
||||||
value={field.state.value}
|
|
||||||
onChange={(event) => field.handleChange(event.target.value)}
|
|
||||||
autoComplete="email"
|
|
||||||
disabled={!isHostedMode}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
{error ? (
|
|
||||||
<p className="mt-1 text-sm text-error">{error}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<form.Field name="password">
|
|
||||||
{(field) => {
|
|
||||||
const error = getFieldError(field.state.meta.errors);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className="input input-bordered w-full"
|
|
||||||
placeholder="Password..."
|
|
||||||
value={field.state.value}
|
|
||||||
onChange={(event) => field.handleChange(event.target.value)}
|
|
||||||
autoComplete="new-password"
|
|
||||||
disabled={!isHostedMode}
|
|
||||||
required
|
|
||||||
minLength={HOSTED_PASSWORD_MIN_LENGTH}
|
|
||||||
maxLength={HOSTED_PASSWORD_MAX_LENGTH}
|
|
||||||
/>
|
|
||||||
{error ? (
|
|
||||||
<p className="mt-1 text-sm text-error">{error}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<form.Field name="confirmPassword">
|
|
||||||
{(field) => {
|
|
||||||
const error = getFieldError(field.state.meta.errors);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className="input input-bordered w-full"
|
|
||||||
placeholder="Confirm password..."
|
|
||||||
value={field.state.value}
|
|
||||||
onChange={(event) => field.handleChange(event.target.value)}
|
|
||||||
autoComplete="new-password"
|
|
||||||
disabled={!isHostedMode}
|
|
||||||
required
|
|
||||||
minLength={HOSTED_PASSWORD_MIN_LENGTH}
|
|
||||||
maxLength={HOSTED_PASSWORD_MAX_LENGTH}
|
|
||||||
/>
|
|
||||||
{error ? (
|
|
||||||
<p className="mt-1 text-sm text-error">{error}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<form.Subscribe
|
|
||||||
selector={(state) => ({
|
|
||||||
submitError: state.errorMap.onSubmit,
|
|
||||||
isSubmitting: state.isSubmitting,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
{({ submitError, isSubmitting }) => {
|
<form.Field name="name">
|
||||||
const errorMessage = getFormError(submitError);
|
{(field) => {
|
||||||
return (
|
const error = getFieldError(field.state.meta.errors);
|
||||||
<>
|
|
||||||
{errorMessage ? (
|
return (
|
||||||
<p className="text-sm text-error">{errorMessage}</p>
|
<div>
|
||||||
) : null}
|
<input
|
||||||
<button
|
type="text"
|
||||||
className="btn btn-soft w-full"
|
className="input input-bordered w-full"
|
||||||
disabled={!isHostedMode || isSubmitting}
|
placeholder="Name (optional)..."
|
||||||
>
|
value={field.state.value}
|
||||||
{isSubmitting ? "Creating account..." : "Create account"}
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
</button>
|
autoComplete="name"
|
||||||
</>
|
disabled={!isHostedMode}
|
||||||
);
|
/>
|
||||||
}}
|
{error ? (
|
||||||
</form.Subscribe>
|
<p className="mt-1 text-sm text-error">{error}</p>
|
||||||
</form>
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="email">
|
||||||
|
{(field) => {
|
||||||
|
const error = getFieldError(field.state.meta.errors);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
placeholder="Email address..."
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
|
autoComplete="email"
|
||||||
|
disabled={!isHostedMode}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{error ? (
|
||||||
|
<p className="mt-1 text-sm text-error">{error}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="password">
|
||||||
|
{(field) => {
|
||||||
|
const error = getFieldError(field.state.meta.errors);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
placeholder="Password..."
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
|
autoComplete="new-password"
|
||||||
|
disabled={!isHostedMode}
|
||||||
|
required
|
||||||
|
minLength={HOSTED_PASSWORD_MIN_LENGTH}
|
||||||
|
maxLength={HOSTED_PASSWORD_MAX_LENGTH}
|
||||||
|
/>
|
||||||
|
{error ? (
|
||||||
|
<p className="mt-1 text-sm text-error">{error}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="confirmPassword">
|
||||||
|
{(field) => {
|
||||||
|
const error = getFieldError(field.state.meta.errors);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
placeholder="Confirm password..."
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
|
autoComplete="new-password"
|
||||||
|
disabled={!isHostedMode}
|
||||||
|
required
|
||||||
|
minLength={HOSTED_PASSWORD_MIN_LENGTH}
|
||||||
|
maxLength={HOSTED_PASSWORD_MAX_LENGTH}
|
||||||
|
/>
|
||||||
|
{error ? (
|
||||||
|
<p className="mt-1 text-sm text-error">{error}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Subscribe
|
||||||
|
selector={(state) => ({
|
||||||
|
submitError: state.errorMap.onSubmit,
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{({ submitError, isSubmitting }) => {
|
||||||
|
const errorMessage = getFormError(submitError);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{errorMessage ? (
|
||||||
|
<p className="text-sm text-error">{errorMessage}</p>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
className="btn btn-soft w-full"
|
||||||
|
disabled={!isHostedMode || isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Creating account..." : "Create account"}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</form.Subscribe>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</AuthPageCard>
|
</AuthPageCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user