diff --git a/public/auth-bg.jpg b/public/auth-bg.jpg
new file mode 100644
index 0000000..1ea03b1
Binary files /dev/null and b/public/auth-bg.jpg differ
diff --git a/src/client/features/auth/AuthPage.tsx b/src/client/features/auth/AuthPage.tsx
index 95e6a0b..821e0d2 100644
--- a/src/client/features/auth/AuthPage.tsx
+++ b/src/client/features/auth/AuthPage.tsx
@@ -1,4 +1,14 @@
import { Link } from "@tanstack/react-router";
+import * as React from "react";
+import {
+ Zap,
+ ShieldCheck,
+ TrendingUp,
+ Lock,
+ ArrowRight,
+ Eye,
+ EyeOff,
+} from "lucide-react";
import { z } from "zod";
import {
getCurrentAuthRedirect,
@@ -21,16 +31,9 @@ export function useAuthPageState(redirect: string | undefined) {
? getOAuthSignedQuery(window.location.search)
: null;
const isHostedMode = isHostedClientAuthMode();
- // `team` mode: email/password sign-in is live, but Google social login and
- // self-serve signup are not.
const isSessionMode = isSessionClientAuthMode();
- return {
- redirectTo,
- oauthQuery,
- isHostedMode,
- isSessionMode,
- };
+ return { redirectTo, oauthQuery, isHostedMode, isSessionMode };
}
export function AuthMethodChooser({
@@ -52,21 +55,20 @@ export function AuthMethodChooser({
- {isBusy ? "Opening Google..." : googleLabel}
+ {isBusy ? "Opening Google..." : googleLabel}
-
- {emailLabel}
+ {emailLabel}
);
@@ -95,6 +97,120 @@ function GoogleLogo() {
);
}
+export function AuthInputField({
+ icon: Icon,
+ type = "text",
+ placeholder,
+ value,
+ onChange,
+ error,
+ autoComplete,
+ required,
+ minLength,
+ maxLength,
+ disabled,
+ showPasswordToggle,
+ onTogglePassword,
+ isPasswordVisible,
+}: {
+ icon: React.ComponentType<{ className?: string }>;
+ type?: string;
+ placeholder?: string;
+ value?: string;
+ onChange?: (e: React.ChangeEvent) => void;
+ error?: string | null;
+ autoComplete?: string;
+ required?: boolean;
+ minLength?: number;
+ maxLength?: number;
+ disabled?: boolean;
+ showPasswordToggle?: boolean;
+ onTogglePassword?: () => void;
+ isPasswordVisible?: boolean;
+}) {
+ return (
+
+
+
+
+
+
+ {showPasswordToggle ? (
+
+ {isPasswordVisible ? (
+
+ ) : (
+
+ )}
+
+ ) : null}
+
+ {error ? (
+
{error}
+ ) : null}
+
+ );
+}
+
+export function AuthSubmitButton({
+ children,
+ isSubmitting,
+ disabled,
+}: {
+ children: React.ReactNode;
+ isSubmitting?: boolean;
+ disabled?: boolean;
+}) {
+ return (
+
+ {isSubmitting ? (
+
+ ) : (
+ <>
+ {children}
+
+ >
+ )}
+
+ );
+}
+
+export function AuthDivider({ text = "or" }: { text?: string }) {
+ return (
+
+ );
+}
+
export function AuthPageCard({
title,
helperText,
@@ -107,30 +223,35 @@ export function AuthPageCard({
footer?: React.ReactNode;
}) {
return (
-
-
+
+
-
-
{title}
- {helperText ? (
-
{helperText}
- ) : null}
-
+
+ {title}
+
+ {helperText ? (
+
+ {helperText}
+
+ ) : null}
- {children}
+
{children}
- {footer ?
{footer}
: null}
+ {footer ?
{footer}
: null}
+
+
+
+ Your information is secure and encrypted.
+
);
}
-// `team` mode has no self-serve signup — the workspace owner provisions
-// accounts. Shown in place of the sign-up form.
export function InviteOnlyCard({ redirectTo }: { redirectTo: string }) {
return (
- Back to sign in
+ Back to sign in
+
);
}
-export function AuthPageShell({ children }: { children: React.ReactNode }) {
+const VALUE_PROPS = [
+ {
+ icon: Zap,
+ title: "Get Started Quickly",
+ desc: "Set up your account in just a few simple steps.",
+ },
+ {
+ icon: ShieldCheck,
+ title: "Your Data is Safe",
+ desc: "Enterprise-grade security to keep your information protected.",
+ },
+ {
+ icon: TrendingUp,
+ title: "Build for What's Next",
+ desc: "Powerful tools and flexible features to help you grow.",
+ },
+];
+
+export function AuthPageShell({
+ children,
+ topLink,
+}: {
+ children: React.ReactNode;
+ topLink?: {
+ text: string;
+ linkText: string;
+ to: string;
+ search?: Record
;
+ };
+}) {
return (
- // `h-[100dvh]` + `overflow-y-auto` makes this a scroll container, and the
- // auto-margin child centers when it fits but stays fully reachable (top and
- // bottom) when it's taller than the viewport. Plain `justify-center` clips
- // the overflow with no way to scroll to it.
-
-
{children}
+
+
+
+
+ {/* Left Column: Brand & Hero Value Props */}
+
+
+
+
+
+ Metatroncube
+
+
+ Solutions
+
+
+
+
+
+
+
+ JOIN OUR PLATFORM
+
+
+
+ Build something
+
+
+ exceptional.
+
+
+
+
+ Create your account and get started in minutes. Join a growing
+ community of creators, builders and innovators.
+
+
+
+ {VALUE_PROPS.map(({ icon: Icon, title, desc }) => (
+
+ ))}
+
+
+
+ "More than software,
+
+ it's a better tomorrow."
+
+
+
+
+
+
+ {/* Right Column: Floating White Card */}
+
+
+ {topLink ? (
+
+
{topLink.text}
+
+
{topLink.linkText}
+
+
+
+ ) : null}
+
+
+
{children}
+
+
+
+ — • • •
+ SIMPLE
+ /
+ SECURE
+ /
+ POWERFUL
+
+
+
);
}
diff --git a/src/client/features/settings/ChangePasswordSection.tsx b/src/client/features/settings/ChangePasswordSection.tsx
new file mode 100644
index 0000000..ed74985
--- /dev/null
+++ b/src/client/features/settings/ChangePasswordSection.tsx
@@ -0,0 +1,167 @@
+import * as React from "react";
+import { Lock, ShieldCheck } from "lucide-react";
+import { toast } from "sonner";
+import { authClient } from "@/lib/auth-client";
+import { HOSTED_PASSWORD_MIN_LENGTH } from "@/lib/auth-options";
+import { AuthInputField, AuthSubmitButton } from "@/client/features/auth/AuthPage";
+
+export function ChangePasswordSection() {
+ const [currentPassword, setCurrentPassword] = React.useState("");
+ const [newPassword, setNewPassword] = React.useState("");
+ const [confirmPassword, setConfirmPassword] = React.useState("");
+ const [showCurrentPassword, setShowCurrentPassword] = React.useState(false);
+ const [showNewPassword, setShowNewPassword] = React.useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = React.useState(false);
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+ const [error, setError] = React.useState
(null);
+
+ async function handleSubmit(event: React.FormEvent) {
+ event.preventDefault();
+ setError(null);
+
+ if (!currentPassword) {
+ setError("Please enter your current password.");
+ return;
+ }
+
+ if (newPassword.length < HOSTED_PASSWORD_MIN_LENGTH) {
+ setError(
+ `New password must be at least ${HOSTED_PASSWORD_MIN_LENGTH} characters.`,
+ );
+ return;
+ }
+
+ if (newPassword !== confirmPassword) {
+ setError("New passwords do not match.");
+ return;
+ }
+
+ if (currentPassword === newPassword) {
+ setError("New password must be different from your current password.");
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ const result = await authClient.changePassword({
+ currentPassword,
+ newPassword,
+ revokeOtherSessions: true,
+ });
+
+ if (result.error) {
+ const msg =
+ result.error.message ||
+ "Failed to update password. Please check your current password.";
+ setError(msg);
+ toast.error(msg);
+ } else {
+ toast.success("Password updated successfully.");
+ setCurrentPassword("");
+ setNewPassword("");
+ setConfirmPassword("");
+ }
+ } catch {
+ setError("An unexpected error occurred while updating your password.");
+ toast.error("Failed to update password.");
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ return (
+
+
+
+
+ Change Password
+
+
+ Update your password to keep your account secure.
+
+
+
+
+
+ );
+}
diff --git a/src/client/styles/app.css b/src/client/styles/app.css
index 7cb240f..9658c1b 100644
--- a/src/client/styles/app.css
+++ b/src/client/styles/app.css
@@ -1,4 +1,4 @@
-@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
+@import url("https://fonts.googleapis.com/css2?family=Caveat:wght@400;600;700&family=Inter:wght@400;500;600;700&display=swap");
@import "tailwindcss";
@theme {
diff --git a/src/env.d.ts b/src/env.d.ts
index 788c269..f1ca843 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -72,6 +72,7 @@ interface ImportMetaEnv {
readonly POSTHOG_PUBLIC_KEY?: string;
readonly POSTHOG_HOST?: string;
readonly TURNSTILE_SITE_KEY?: string;
+ readonly GOOGLE_CLIENT_ID?: string;
readonly VITE_E2E_DOMAIN_FIXTURES?: string;
readonly VITE_E2E_KEYWORD_FIXTURES?: string;
}
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index bd2a495..84dda5a 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -179,10 +179,15 @@ function createAuth() {
// Reset-by-email is hosted-only (Loops). In `team` mode the owner
// resets a member's password from the admin screen instead.
if (!isHostedAuthMode(env.AUTH_MODE)) return;
- await sendHostedPasswordResetEmail({
- email: user.email,
- resetUrl: url,
- });
+ try {
+ await sendHostedPasswordResetEmail({
+ email: user.email,
+ resetUrl: url,
+ });
+ } catch (error) {
+ console.error("[auth] Failed to send password reset email via Loops:", error);
+ console.log(`[auth] Password reset URL for ${user.email}: ${url}`);
+ }
},
},
emailVerification:
@@ -191,10 +196,15 @@ function createAuth() {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, url }) => {
- await sendHostedVerificationEmail({
- email: user.email,
- confirmationUrl: url,
- });
+ try {
+ await sendHostedVerificationEmail({
+ email: user.email,
+ confirmationUrl: url,
+ });
+ } catch (error) {
+ console.error("[auth] Failed to send verification email via Loops:", error);
+ console.log(`[auth] Verification URL for ${user.email}: ${url}`);
+ }
},
}
: undefined,
@@ -310,6 +320,10 @@ function getTrustedOrigins(baseUrl: string) {
"http://*.open-seo.localhost:1355",
"https://open-seo.localhost:1355",
"https://*.open-seo.localhost:1355",
+ "http://localhost:3001",
+ "http://127.0.0.1:3001",
+ "http://localhost:3000",
+ "http://127.0.0.1:3000",
);
}
@@ -344,30 +358,20 @@ function getHostedSecret() {
}
function getSocialProviders() {
- // Google social login is hosted-only. Self-hosted builds the auth instance
- // solely for Search Console token ops, which use the genericOAuth provider
- // (createBaseAuthConfig) with its own creds — so it must NOT require the
- // social-login config here, otherwise getAuth() construction would be coupled
- // to GSC creds rather than just BETTER_AUTH_SECRET.
if (!isHostedAuthMode(env.AUTH_MODE)) {
return {};
}
- return {
- google: getGoogleSocialProviderConfig(),
- };
+ const google = getGoogleSocialProviderConfig();
+ return google ? { google } : {};
}
function getGoogleSocialProviderConfig() {
const googleClientId = env.GOOGLE_CLIENT_ID?.trim();
const googleClientSecret = env.GOOGLE_CLIENT_SECRET?.trim();
- if (!googleClientId) {
- throw new Error("GOOGLE_CLIENT_ID is required in hosted mode");
- }
-
- if (!googleClientSecret) {
- throw new Error("GOOGLE_CLIENT_SECRET is required in hosted mode");
+ if (!googleClientId || !googleClientSecret) {
+ return undefined;
}
return {
@@ -396,7 +400,6 @@ function hasHostedAuthConfig() {
try {
getHostedBaseUrl();
getHostedSecret();
- getGoogleSocialProviderConfig();
return (
hasHostedTurnstileConfig(env) &&
(Reflect.get(env, "BYPASS_EMAIL_VERIFICATION") === "true" ||
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index f891dd3..0ad1c37 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as VerifyEmailRouteImport } from './routes/verify-email'
import { Route as ResetPasswordRouteImport } from './routes/reset-password'
import { Route as ForgotPasswordRouteImport } from './routes/forgot-password'
+import { Route as ChangePasswordRouteImport } from './routes/change-password'
import { Route as AuthErrorRouteImport } from './routes/auth-error'
import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
import { Route as AuthRouteImport } from './routes/_auth'
@@ -82,6 +83,11 @@ const ForgotPasswordRoute = ForgotPasswordRouteImport.update({
path: '/forgot-password',
getParentRoute: () => rootRouteImport,
} as any)
+const ChangePasswordRoute = ChangePasswordRouteImport.update({
+ id: '/change-password',
+ path: '/change-password',
+ getParentRoute: () => rootRouteImport,
+} as any)
const AuthErrorRoute = AuthErrorRouteImport.update({
id: '/auth-error',
path: '/auth-error',
@@ -370,6 +376,7 @@ const ProjectPProjectIdAuditIssuesResultIdRoute =
export interface FileRoutesByFullPath {
'/': typeof AppIndexRoute
'/auth-error': typeof AuthErrorRoute
+ '/change-password': typeof ChangePasswordRoute
'/forgot-password': typeof ForgotPasswordRoute
'/reset-password': typeof ResetPasswordRoute
'/verify-email': typeof VerifyEmailRoute
@@ -425,6 +432,7 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof AppIndexRoute
'/auth-error': typeof AuthErrorRoute
+ '/change-password': typeof ChangePasswordRoute
'/forgot-password': typeof ForgotPasswordRoute
'/reset-password': typeof ResetPasswordRoute
'/verify-email': typeof VerifyEmailRoute
@@ -479,6 +487,7 @@ export interface FileRoutesById {
'/_auth': typeof AuthRouteWithChildren
'/_authenticated': typeof AuthenticatedRouteWithChildren
'/auth-error': typeof AuthErrorRoute
+ '/change-password': typeof ChangePasswordRoute
'/forgot-password': typeof ForgotPasswordRoute
'/reset-password': typeof ResetPasswordRoute
'/verify-email': typeof VerifyEmailRoute
@@ -537,6 +546,7 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/auth-error'
+ | '/change-password'
| '/forgot-password'
| '/reset-password'
| '/verify-email'
@@ -592,6 +602,7 @@ export interface FileRouteTypes {
to:
| '/'
| '/auth-error'
+ | '/change-password'
| '/forgot-password'
| '/reset-password'
| '/verify-email'
@@ -645,6 +656,7 @@ export interface FileRouteTypes {
| '/_auth'
| '/_authenticated'
| '/auth-error'
+ | '/change-password'
| '/forgot-password'
| '/reset-password'
| '/verify-email'
@@ -705,6 +717,7 @@ export interface RootRouteChildren {
AuthRoute: typeof AuthRouteWithChildren
AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
AuthErrorRoute: typeof AuthErrorRoute
+ ChangePasswordRoute: typeof ChangePasswordRoute
ForgotPasswordRoute: typeof ForgotPasswordRoute
ResetPasswordRoute: typeof ResetPasswordRoute
VerifyEmailRoute: typeof VerifyEmailRoute
@@ -741,6 +754,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ForgotPasswordRouteImport
parentRoute: typeof rootRouteImport
}
+ '/change-password': {
+ id: '/change-password'
+ path: '/change-password'
+ fullPath: '/change-password'
+ preLoaderRoute: typeof ChangePasswordRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/auth-error': {
id: '/auth-error'
path: '/auth-error'
@@ -1313,6 +1333,7 @@ const rootRouteChildren: RootRouteChildren = {
AuthRoute: AuthRouteWithChildren,
AuthenticatedRoute: AuthenticatedRouteWithChildren,
AuthErrorRoute: AuthErrorRoute,
+ ChangePasswordRoute: ChangePasswordRoute,
ForgotPasswordRoute: ForgotPasswordRoute,
ResetPasswordRoute: ResetPasswordRoute,
VerifyEmailRoute: VerifyEmailRoute,
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 0d7b772..94ffc70 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -148,7 +148,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<>
-
+ {isHostedClientAuthMode() ? : null}
{children}
+ {isSessionClientAuthMode() && Boolean(session?.user) ? (
+
+ ) : null}
+
Analytics
diff --git a/src/routes/_auth.sign-in.tsx b/src/routes/_auth.sign-in.tsx
index e3008b1..7aaeea1 100644
--- a/src/routes/_auth.sign-in.tsx
+++ b/src/routes/_auth.sign-in.tsx
@@ -1,9 +1,13 @@
import { useForm } from "@tanstack/react-form";
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react";
+import { Mail, Lock, ArrowRight } from "lucide-react";
import {
AuthPageCard,
AuthMethodChooser,
+ AuthInputField,
+ AuthSubmitButton,
+ AuthDivider,
authRedirectSearchSchema,
useAuthPageState,
} from "@/client/features/auth/AuthPage";
@@ -30,8 +34,10 @@ function SignInPage() {
const { redirectTo, oauthQuery, isHostedMode, isSessionMode } =
useAuthPageState(search.redirect);
const authCallbackURL = redirectTo;
- // `team` mode has no Google button, so go straight to the email/password form.
- const [showEmailForm, setShowEmailForm] = useState(!isHostedMode);
+ const hasGoogleAuth =
+ isHostedMode && Boolean(import.meta.env.GOOGLE_CLIENT_ID?.trim());
+ const [showEmailForm, setShowEmailForm] = useState(!hasGoogleAuth);
+ const [showPassword, setShowPassword] = useState(false);
// `team` mode with no owner yet: send them to the one-time setup screen.
useEffect(() => {
@@ -136,32 +142,23 @@ function SignInPage() {
return (
- {showEmailForm ? (
+
+
+
+ Don't have an account?{" "}
- Forgot password?
+ Create account
+
- ) : null}
-
- Create account
-
+
) : null
}
@@ -181,12 +178,12 @@ function SignInPage() {
}}
/>
{socialError ? (
- {socialError}
+ {socialError}
) : null}
>
) : (
+ }
+ >
+ {!isPending && !session?.user?.id ? (
+
+
+ Please sign in to change your account password.
+
+
+
Sign in to continue
+
+
+
+ ) : (
+
+ {error ? (
+ {error}
+ ) : null}
+
+ setCurrentPassword(e.target.value)}
+ required
+ autoComplete="current-password"
+ showPasswordToggle
+ isPasswordVisible={showCurrentPassword}
+ onTogglePassword={() =>
+ setShowCurrentPassword(!showCurrentPassword)
+ }
+ />
+
+ setNewPassword(e.target.value)}
+ required
+ minLength={HOSTED_PASSWORD_MIN_LENGTH}
+ autoComplete="new-password"
+ showPasswordToggle
+ isPasswordVisible={showNewPassword}
+ onTogglePassword={() => setShowNewPassword(!showNewPassword)}
+ />
+
+ setConfirmPassword(e.target.value)}
+ required
+ minLength={HOSTED_PASSWORD_MIN_LENGTH}
+ autoComplete="new-password"
+ showPasswordToggle
+ isPasswordVisible={showConfirmPassword}
+ onTogglePassword={() =>
+ setShowConfirmPassword(!showConfirmPassword)
+ }
+ />
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/routes/forgot-password.tsx b/src/routes/forgot-password.tsx
index e16e351..3e02550 100644
--- a/src/routes/forgot-password.tsx
+++ b/src/routes/forgot-password.tsx
@@ -1,8 +1,12 @@
import { useForm } from "@tanstack/react-form";
import { Link, createFileRoute } from "@tanstack/react-router";
+import { Mail, ArrowRight } from "lucide-react";
import {
AuthPageCard,
AuthPageShell,
+ AuthInputField,
+ AuthSubmitButton,
+ AuthDivider,
authRedirectSearchSchema,
} from "@/client/features/auth/AuthPage";
import { getFieldError, getFormError } from "@/client/lib/forms";
@@ -63,7 +67,14 @@ function ForgotPasswordPage() {
});
return (
-
+
({
isSuccess: state.isSubmitSuccessful && !state.errorMap.onSubmit,
@@ -77,36 +88,48 @@ function ForgotPasswordPage() {
return (
-
- Back to sign in
-
-
+
+
+
+
+ Back to sign in
+
+
+
+
}
>
{isSuccess ? (
-
-
+
+
If an account exists for that email, you'll receive password
reset instructions shortly.
-
+
+
+
Back to sign in
+
+
) : (
{
event.preventDefault();
void form.handleSubmit();
@@ -117,36 +140,35 @@ function ForgotPasswordPage() {
const error = getFieldError(field.state.meta.errors);
return (
-
-
- field.handleChange(event.target.value)
- }
- autoComplete="email"
- disabled={!isHostedMode}
- required
- />
- {error ? (
-
{error}
- ) : null}
-
+
+ field.handleChange(event.target.value)
+ }
+ autoComplete="email"
+ disabled={!isHostedMode}
+ required
+ error={error}
+ />
);
}}
{errorMessage ? (
- {errorMessage}
+
+ {errorMessage}
+
) : null}
-
- {isSubmitting ? "Sending reset link..." : "Send reset link"}
-
+ Send reset link
+
)}
diff --git a/src/routes/reset-password.tsx b/src/routes/reset-password.tsx
index d098e7b..df591c5 100644
--- a/src/routes/reset-password.tsx
+++ b/src/routes/reset-password.tsx
@@ -1,8 +1,13 @@
import { useForm } from "@tanstack/react-form";
import { Link, createFileRoute } from "@tanstack/react-router";
+import { useState } from "react";
+import { Lock, ArrowRight } from "lucide-react";
import {
AuthPageCard,
AuthPageShell,
+ AuthInputField,
+ AuthSubmitButton,
+ AuthDivider,
authRedirectSearchSchema,
} from "@/client/features/auth/AuthPage";
import { getFieldError, getFormError } from "@/client/lib/forms";
@@ -104,6 +109,9 @@ function ResetPasswordPage() {
const isHostedMode = isHostedClientAuthMode();
const routeError = getResetPasswordErrorMessage(search.error);
const token = typeof search.token === "string" ? search.token : null;
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+
const form = useForm({
defaultValues: {
password: "",
@@ -150,7 +158,14 @@ function ResetPasswordPage() {
});
return (
-
+
({
isComplete: state.isSubmitSuccessful && !state.errorMap.onSubmit,
@@ -172,39 +187,52 @@ function ResetPasswordPage() {
title={pageCopy.title}
helperText={pageCopy.helperText}
footer={
-
-
- Sign in
-
-
+
}
>
{!isHostedMode ? null : isComplete ? (
-
- Continue to sign in
-
+
) : routeError || !token ? (
-
- Request a new reset link
-
+
+
+
Request a new reset link
+
+
+
) : (
{
event.preventDefault();
void form.handleSubmit();
@@ -215,24 +243,25 @@ function ResetPasswordPage() {
const error = getFieldError(field.state.meta.errors);
return (
-
-
- field.handleChange(event.target.value)
- }
- autoComplete="new-password"
- minLength={HOSTED_PASSWORD_MIN_LENGTH}
- maxLength={HOSTED_PASSWORD_MAX_LENGTH}
- required
- />
- {error ? (
-
{error}
- ) : null}
-
+
+ field.handleChange(event.target.value)
+ }
+ autoComplete="new-password"
+ minLength={HOSTED_PASSWORD_MIN_LENGTH}
+ maxLength={HOSTED_PASSWORD_MAX_LENGTH}
+ required
+ showPasswordToggle
+ isPasswordVisible={showPassword}
+ onTogglePassword={() =>
+ setShowPassword(!showPassword)
+ }
+ error={error}
+ />
);
}}
@@ -242,37 +271,41 @@ function ResetPasswordPage() {
const error = getFieldError(field.state.meta.errors);
return (
-
-
- field.handleChange(event.target.value)
- }
- autoComplete="new-password"
- minLength={HOSTED_PASSWORD_MIN_LENGTH}
- maxLength={HOSTED_PASSWORD_MAX_LENGTH}
- required
- />
- {error ? (
-
{error}
- ) : null}
-
+
+ field.handleChange(event.target.value)
+ }
+ autoComplete="new-password"
+ minLength={HOSTED_PASSWORD_MIN_LENGTH}
+ maxLength={HOSTED_PASSWORD_MAX_LENGTH}
+ required
+ showPasswordToggle
+ isPasswordVisible={showConfirmPassword}
+ onTogglePassword={() =>
+ setShowConfirmPassword(!showConfirmPassword)
+ }
+ error={error}
+ />
);
}}
{errorMessage ? (
- {errorMessage}
+
+ {errorMessage}
+
) : null}
-
- {isSubmitting ? "Updating password..." : "Update password"}
-
+ Update password
+
)}
diff --git a/src/types/vite-env.d.ts b/src/types/vite-env.d.ts
index 51380db..4a12148 100644
--- a/src/types/vite-env.d.ts
+++ b/src/types/vite-env.d.ts
@@ -8,6 +8,7 @@ interface ViteTypeOptions {
interface ImportMetaEnv {
readonly VITE_SHOW_DEVTOOLS?: string;
readonly BYPASS_EMAIL_VERIFICATION?: string;
+ readonly GOOGLE_CLIENT_ID?: string;
// more env variables...
}
diff --git a/vite.config.ts b/vite.config.ts
index f329d97..1f317a4 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -24,6 +24,7 @@ export default defineConfig(() => {
"POSTHOG_PUBLIC_KEY",
"POSTHOG_HOST",
"TURNSTILE_SITE_KEY",
+ "GOOGLE_CLIENT_ID",
],
server: {