redesign: auth pages (#60)
Remove card/shadow layout in favor of a clean centered design. Add tree logo, simplify inputs (placeholder-only, no labels), use soft buttons, narrow form width, and clean up footer links. * fix: remove unused AUTUMN_SEO_DATA_USAGE_FEATURE_ID export This constant became unused after the billing buckets separation in #57 which replaced it with separate balance/topup feature IDs. * fix: resolve oxlint errors from billing buckets PR Add eslint-disable for max-lines in HostedBillingContent.tsx. Type trackMock properly in dataforseoClient.test.ts to eliminate unsafe type assertions without needing eslint-disable.
This commit is contained in:
parent
25255923b8
commit
5617c6b9f3
BIN
public/transparent-logo.png
Normal file
BIN
public/transparent-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@ -38,32 +38,37 @@ export function AuthPageCard({
|
||||
footer,
|
||||
}: {
|
||||
title: string;
|
||||
helperText: string;
|
||||
helperText?: string;
|
||||
children: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="card w-full max-w-md bg-base-100 shadow-xl border border-base-300">
|
||||
<div className="card-body gap-4">
|
||||
<div className="w-full max-w-xs space-y-6">
|
||||
<div className="text-center space-y-3">
|
||||
<img
|
||||
src="/transparent-logo.png"
|
||||
alt="OpenSEO"
|
||||
className="mx-auto size-10 rounded-lg"
|
||||
/>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">{title}</h1>
|
||||
<p className="text-sm text-base-content/70 mt-1">{helperText}</p>
|
||||
<h1 className="text-xl font-semibold">{title}</h1>
|
||||
{helperText ? (
|
||||
<p className="text-sm text-base-content/60 mt-1">{helperText}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
|
||||
{footer}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
|
||||
{footer ? <div className="text-center">{footer}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AuthPageShell({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-[100dvh] bg-base-200">
|
||||
<div className="min-h-[100dvh] flex items-center justify-center p-4">
|
||||
{children}
|
||||
</div>
|
||||
<div className="min-h-[100dvh] flex flex-col items-center justify-center p-4 bg-base-100">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
import type { UseCustomerResult } from "autumn-js/react";
|
||||
import { ExternalLink, LoaderCircle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
@ -23,20 +23,11 @@ export const Route = createFileRoute("/_auth/sign-in")({
|
||||
component: SignInPage,
|
||||
});
|
||||
|
||||
function getHelperText(isHostedMode: boolean) {
|
||||
if (!isHostedMode) {
|
||||
return "Sign-in isn't available right now.";
|
||||
}
|
||||
|
||||
return "Sign in to your OpenSEO account.";
|
||||
}
|
||||
|
||||
function SignInPage() {
|
||||
const search = Route.useSearch();
|
||||
const { redirectTo, isHostedMode, isSessionPending } = useAuthPageState(
|
||||
search.redirect,
|
||||
);
|
||||
const helperText = getHelperText(isHostedMode);
|
||||
const [verificationEmail, setVerificationEmail] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
@ -127,19 +118,24 @@ function SignInPage() {
|
||||
return (
|
||||
<AuthPageCard
|
||||
title="Sign in"
|
||||
helperText={helperText}
|
||||
footer={
|
||||
isHostedMode ? (
|
||||
<p className="text-sm text-base-content/70">
|
||||
Need an account?{" "}
|
||||
<div className="flex justify-between text-sm text-base-content/50">
|
||||
<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>
|
||||
<Link
|
||||
to="/sign-up"
|
||||
search={getSignInSearch(redirectTo)}
|
||||
className="link link-primary"
|
||||
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
|
||||
>
|
||||
Create account
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
@ -150,69 +146,53 @@ function SignInPage() {
|
||||
void form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<label className="form-control block">
|
||||
<span className="label-text text-sm font-medium">Email</span>
|
||||
<form.Field name="email">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
<form.Field name="email">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="email"
|
||||
className="input input-bordered w-full mt-1"
|
||||
placeholder="you@example.com"
|
||||
value={field.state.value}
|
||||
onChange={(event) => field.handleChange(event.target.value)}
|
||||
autoComplete="email"
|
||||
disabled={!isHostedMode || isSessionPending}
|
||||
required
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</label>
|
||||
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 || isSessionPending}
|
||||
required
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
|
||||
<label className="form-control block">
|
||||
<span className="label-text text-sm font-medium">Password</span>
|
||||
<form.Field name="password">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
<form.Field name="password">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="password"
|
||||
className="input input-bordered w-full mt-1"
|
||||
placeholder="Enter your password"
|
||||
value={field.state.value}
|
||||
onChange={(event) => field.handleChange(event.target.value)}
|
||||
autoComplete="current-password"
|
||||
disabled={!isHostedMode || isSessionPending}
|
||||
required
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</label>
|
||||
|
||||
<div className="text-right">
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
search={getSignInSearch(redirectTo)}
|
||||
className="link link-hover text-sm"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
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 || isSessionPending}
|
||||
required
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
|
||||
{verificationEmail ? (
|
||||
<div className="alert alert-warning items-start">
|
||||
@ -251,7 +231,7 @@ function SignInPage() {
|
||||
<p className="text-sm text-error">{errorMessage}</p>
|
||||
) : null}
|
||||
<button
|
||||
className="btn btn-primary w-full"
|
||||
className="btn btn-soft w-full"
|
||||
disabled={!isHostedMode || isSessionPending || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Signing in..." : "Sign in"}
|
||||
|
||||
@ -41,19 +41,12 @@ export const Route = createFileRoute("/_auth/sign-up")({
|
||||
component: SignUpPage,
|
||||
});
|
||||
|
||||
function getHelperText(isHostedMode: boolean) {
|
||||
return isHostedMode
|
||||
? "Create your OpenSEO account."
|
||||
: "Account creation is only available when AUTH_MODE=hosted.";
|
||||
}
|
||||
|
||||
function SignUpPage() {
|
||||
const search = Route.useSearch();
|
||||
const navigate = useNavigate();
|
||||
const { redirectTo, isHostedMode, isSessionPending } = useAuthPageState(
|
||||
search.redirect,
|
||||
);
|
||||
const helperText = getHelperText(isHostedMode);
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
@ -109,16 +102,15 @@ function SignUpPage() {
|
||||
|
||||
return (
|
||||
<AuthPageCard
|
||||
title="Create account"
|
||||
helperText={helperText}
|
||||
title="Create your account"
|
||||
footer={
|
||||
isHostedMode ? (
|
||||
<p className="text-sm text-base-content/70">
|
||||
<p className="text-sm text-base-content/50">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
to="/sign-in"
|
||||
search={getSignInSearch(redirectTo)}
|
||||
className="link link-primary"
|
||||
className="text-base-content underline underline-offset-2 hover:text-base-content/80 transition-colors"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
@ -133,118 +125,104 @@ function SignUpPage() {
|
||||
void form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<label className="form-control block">
|
||||
<span className="label-text text-sm font-medium">Name</span>
|
||||
<form.Field name="name">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
<form.Field name="name">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
className="input input-bordered w-full mt-1"
|
||||
placeholder="Jane Doe (optional)"
|
||||
value={field.state.value}
|
||||
onChange={(event) => field.handleChange(event.target.value)}
|
||||
autoComplete="name"
|
||||
disabled={!isHostedMode || isSessionPending}
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</label>
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="input input-bordered w-full"
|
||||
placeholder="Name (optional)..."
|
||||
value={field.state.value}
|
||||
onChange={(event) => field.handleChange(event.target.value)}
|
||||
autoComplete="name"
|
||||
disabled={!isHostedMode || isSessionPending}
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
|
||||
<label className="form-control block">
|
||||
<span className="label-text text-sm font-medium">Email</span>
|
||||
<form.Field name="email">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
<form.Field name="email">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="email"
|
||||
className="input input-bordered w-full mt-1"
|
||||
placeholder="you@example.com"
|
||||
value={field.state.value}
|
||||
onChange={(event) => field.handleChange(event.target.value)}
|
||||
autoComplete="email"
|
||||
disabled={!isHostedMode || isSessionPending}
|
||||
required
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</label>
|
||||
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 || isSessionPending}
|
||||
required
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
|
||||
<label className="form-control block">
|
||||
<span className="label-text text-sm font-medium">Password</span>
|
||||
<form.Field name="password">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
<form.Field name="password">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="password"
|
||||
className="input input-bordered w-full mt-1"
|
||||
placeholder="Create a password"
|
||||
value={field.state.value}
|
||||
onChange={(event) => field.handleChange(event.target.value)}
|
||||
autoComplete="new-password"
|
||||
disabled={!isHostedMode || isSessionPending}
|
||||
required
|
||||
minLength={HOSTED_PASSWORD_MIN_LENGTH}
|
||||
maxLength={HOSTED_PASSWORD_MAX_LENGTH}
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</label>
|
||||
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 || isSessionPending}
|
||||
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>
|
||||
|
||||
<label className="form-control block">
|
||||
<span className="label-text text-sm font-medium">
|
||||
Confirm password
|
||||
</span>
|
||||
<form.Field name="confirmPassword">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
<form.Field name="confirmPassword">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="password"
|
||||
className="input input-bordered w-full mt-1"
|
||||
placeholder="Confirm your password"
|
||||
value={field.state.value}
|
||||
onChange={(event) => field.handleChange(event.target.value)}
|
||||
autoComplete="new-password"
|
||||
disabled={!isHostedMode || isSessionPending}
|
||||
required
|
||||
minLength={HOSTED_PASSWORD_MIN_LENGTH}
|
||||
maxLength={HOSTED_PASSWORD_MAX_LENGTH}
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</label>
|
||||
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 || isSessionPending}
|
||||
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) => ({
|
||||
@ -260,7 +238,7 @@ function SignUpPage() {
|
||||
<p className="text-sm text-error">{errorMessage}</p>
|
||||
) : null}
|
||||
<button
|
||||
className="btn btn-primary w-full"
|
||||
className="btn btn-soft w-full"
|
||||
disabled={!isHostedMode || isSessionPending || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Creating account..." : "Create account"}
|
||||
|
||||
@ -87,12 +87,11 @@ function ForgotPasswordPage() {
|
||||
: "Password reset isn't available right now."
|
||||
}
|
||||
footer={
|
||||
<p className="text-sm text-base-content/70">
|
||||
Remembered it?{" "}
|
||||
<p className="text-sm">
|
||||
<Link
|
||||
to="/sign-in"
|
||||
search={getSignInSearch(redirectTo)}
|
||||
className="link link-primary"
|
||||
className="text-base-content/50 hover:text-base-content transition-colors"
|
||||
>
|
||||
Back to sign in
|
||||
</Link>
|
||||
@ -114,42 +113,37 @@ function ForgotPasswordPage() {
|
||||
void form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<label className="form-control block">
|
||||
<span className="label-text text-sm font-medium">
|
||||
Email
|
||||
</span>
|
||||
<form.Field name="email">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
<form.Field name="email">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="email"
|
||||
className="input input-bordered w-full mt-1"
|
||||
placeholder="you@example.com"
|
||||
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}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</label>
|
||||
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>
|
||||
|
||||
{errorMessage ? (
|
||||
<p className="text-sm text-error">{errorMessage}</p>
|
||||
) : null}
|
||||
<button
|
||||
className="btn btn-primary w-full"
|
||||
className="btn btn-soft w-full"
|
||||
disabled={!isHostedMode || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Sending reset link..." : "Send reset link"}
|
||||
|
||||
@ -173,14 +173,13 @@ function ResetPasswordPage() {
|
||||
title={pageCopy.title}
|
||||
helperText={pageCopy.helperText}
|
||||
footer={
|
||||
<p className="text-sm text-base-content/70">
|
||||
Back to{" "}
|
||||
<p className="text-sm">
|
||||
<Link
|
||||
to="/sign-in"
|
||||
search={getSignInSearch(redirectTo)}
|
||||
className="link link-primary"
|
||||
className="text-base-content/50 hover:text-base-content transition-colors"
|
||||
>
|
||||
sign in
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
}
|
||||
@ -192,7 +191,7 @@ function ResetPasswordPage() {
|
||||
? "/sign-in"
|
||||
: `/sign-in?redirect=${encodeURIComponent(redirectTo)}`
|
||||
}
|
||||
className="btn btn-primary w-full"
|
||||
className="btn btn-soft w-full"
|
||||
>
|
||||
Continue to sign in
|
||||
</a>
|
||||
@ -200,7 +199,7 @@ function ResetPasswordPage() {
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
search={getSignInSearch(redirectTo)}
|
||||
className="btn btn-primary w-full"
|
||||
className="btn btn-soft w-full"
|
||||
>
|
||||
Request a new reset link
|
||||
</Link>
|
||||
@ -212,75 +211,65 @@ function ResetPasswordPage() {
|
||||
void form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<label className="form-control block">
|
||||
<span className="label-text text-sm font-medium">
|
||||
New password
|
||||
</span>
|
||||
<form.Field name="password">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
<form.Field name="password">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="password"
|
||||
className="input input-bordered w-full mt-1"
|
||||
placeholder="Create a new password"
|
||||
value={field.state.value}
|
||||
onChange={(event) =>
|
||||
field.handleChange(event.target.value)
|
||||
}
|
||||
autoComplete="new-password"
|
||||
minLength={HOSTED_PASSWORD_MIN_LENGTH}
|
||||
maxLength={HOSTED_PASSWORD_MAX_LENGTH}
|
||||
required
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</label>
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
className="input input-bordered w-full"
|
||||
placeholder="New password..."
|
||||
value={field.state.value}
|
||||
onChange={(event) =>
|
||||
field.handleChange(event.target.value)
|
||||
}
|
||||
autoComplete="new-password"
|
||||
minLength={HOSTED_PASSWORD_MIN_LENGTH}
|
||||
maxLength={HOSTED_PASSWORD_MAX_LENGTH}
|
||||
required
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
|
||||
<label className="form-control block">
|
||||
<span className="label-text text-sm font-medium">
|
||||
Confirm password
|
||||
</span>
|
||||
<form.Field name="confirmPassword">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
<form.Field name="confirmPassword">
|
||||
{(field) => {
|
||||
const error = getFieldError(field.state.meta.errors);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="password"
|
||||
className="input input-bordered w-full mt-1"
|
||||
placeholder="Confirm your new password"
|
||||
value={field.state.value}
|
||||
onChange={(event) =>
|
||||
field.handleChange(event.target.value)
|
||||
}
|
||||
autoComplete="new-password"
|
||||
minLength={HOSTED_PASSWORD_MIN_LENGTH}
|
||||
maxLength={HOSTED_PASSWORD_MAX_LENGTH}
|
||||
required
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</label>
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
className="input input-bordered w-full"
|
||||
placeholder="Confirm new password..."
|
||||
value={field.state.value}
|
||||
onChange={(event) =>
|
||||
field.handleChange(event.target.value)
|
||||
}
|
||||
autoComplete="new-password"
|
||||
minLength={HOSTED_PASSWORD_MIN_LENGTH}
|
||||
maxLength={HOSTED_PASSWORD_MAX_LENGTH}
|
||||
required
|
||||
/>
|
||||
{error ? (
|
||||
<p className="mt-1 text-sm text-error">{error}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
|
||||
{errorMessage ? (
|
||||
<p className="text-sm text-error">{errorMessage}</p>
|
||||
) : null}
|
||||
<button
|
||||
className="btn btn-primary w-full"
|
||||
className="btn btn-soft w-full"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Updating password..." : "Update password"}
|
||||
|
||||
@ -151,14 +151,13 @@ function VerifyEmailPage() {
|
||||
title={pageCopy.title}
|
||||
helperText={pageCopy.helperText}
|
||||
footer={
|
||||
<p className="text-sm text-base-content/70">
|
||||
Need to sign in instead?{" "}
|
||||
<p className="text-sm">
|
||||
<Link
|
||||
to="/sign-in"
|
||||
search={getSignInSearch(redirectTo)}
|
||||
className="link link-primary"
|
||||
className="text-base-content/50 hover:text-base-content transition-colors"
|
||||
>
|
||||
Open sign in
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
}
|
||||
@ -171,7 +170,7 @@ function VerifyEmailPage() {
|
||||
<Link
|
||||
to="/sign-in"
|
||||
search={getSignInSearch(redirectTo)}
|
||||
className="btn btn-primary w-full"
|
||||
className="btn btn-soft w-full"
|
||||
>
|
||||
Back to sign in
|
||||
</Link>
|
||||
@ -186,7 +185,7 @@ function VerifyEmailPage() {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline w-full"
|
||||
className="btn btn-soft w-full"
|
||||
onClick={() => void handleResend()}
|
||||
disabled={isResending}
|
||||
>
|
||||
@ -205,7 +204,7 @@ function VerifyEmailPage() {
|
||||
<Link
|
||||
to="/sign-in"
|
||||
search={getSignInSearch(redirectTo)}
|
||||
className="btn btn-primary w-full"
|
||||
className="btn btn-soft w-full"
|
||||
>
|
||||
Sign in to continue
|
||||
</Link>
|
||||
|
||||
@ -4,10 +4,17 @@ import {
|
||||
AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID,
|
||||
} from "@/shared/billing";
|
||||
|
||||
interface TrackCallArg {
|
||||
customerId: string;
|
||||
featureId: string;
|
||||
value: number;
|
||||
properties?: { balanceFeatureId: string };
|
||||
}
|
||||
|
||||
const { checkMock, trackMock, getOrCreateMock, isHostedServerAuthModeMock } =
|
||||
vi.hoisted(() => ({
|
||||
checkMock: vi.fn(),
|
||||
trackMock: vi.fn(),
|
||||
trackMock: vi.fn<(arg: TrackCallArg) => void>(),
|
||||
getOrCreateMock: vi.fn(),
|
||||
isHostedServerAuthModeMock: vi.fn(),
|
||||
}));
|
||||
@ -218,23 +225,17 @@ describe("meterDataforseoCall with split balances", () => {
|
||||
await client.backlinks.summary(backlinksInput);
|
||||
|
||||
const monthlyCall = trackMock.mock.calls.find(
|
||||
(call: unknown[]) =>
|
||||
(call[0] as { featureId: string }).featureId ===
|
||||
AUTUMN_SEO_DATA_BALANCE_FEATURE_ID,
|
||||
([arg]) => arg.featureId === AUTUMN_SEO_DATA_BALANCE_FEATURE_ID,
|
||||
);
|
||||
const topupCall = trackMock.mock.calls.find(
|
||||
(call: unknown[]) =>
|
||||
(call[0] as { featureId: string }).featureId ===
|
||||
AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID,
|
||||
([arg]) => arg.featureId === AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID,
|
||||
);
|
||||
|
||||
expect(
|
||||
(monthlyCall?.[0] as { properties: { balanceFeatureId: string } })
|
||||
.properties.balanceFeatureId,
|
||||
).toBe(AUTUMN_SEO_DATA_BALANCE_FEATURE_ID);
|
||||
expect(
|
||||
(topupCall?.[0] as { properties: { balanceFeatureId: string } })
|
||||
.properties.balanceFeatureId,
|
||||
).toBe(AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID);
|
||||
expect(monthlyCall![0].properties?.balanceFeatureId).toBe(
|
||||
AUTUMN_SEO_DATA_BALANCE_FEATURE_ID,
|
||||
);
|
||||
expect(topupCall![0].properties?.balanceFeatureId).toBe(
|
||||
AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -6,7 +6,6 @@ export const AUTUMN_MANAGED_SERVICE_ACCESS_FEATURE_ID =
|
||||
"managed_service_access";
|
||||
export const AUTUMN_SEO_DATA_BALANCE_FEATURE_ID = "usage_credits";
|
||||
export const AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID = "topup_credits";
|
||||
export const AUTUMN_SEO_DATA_USAGE_FEATURE_ID = "seo_data_usage";
|
||||
export const AUTUMN_SEO_DATA_CREDITS_PER_USD = 1000;
|
||||
export const MINIMUM_SEO_DATA_BALANCE_USD = 0.15;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user