Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions app/components/forms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,20 @@ export function OTPField({
errors,
className,
type,
slotClassName,
groupClassName,
separatorClassName,
showSeparator = true,
}: {
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
inputProps: Partial<OTPInputProps & { render: never }>
errors?: ListOfErrors
className?: string
type: 'digits' | 'digits-and-characters'
slotClassName?: string
groupClassName?: string
separatorClassName?: string
showSeparator?: boolean
}) {
const fallbackId = useId()
const id = inputProps.id ?? fallbackId
Expand All @@ -138,17 +146,19 @@ export function OTPField({
aria-describedby={errorId}
{...inputProps}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
<InputOTPGroup className={groupClassName}>
<InputOTPSlot className={slotClassName} index={0} />
<InputOTPSlot className={slotClassName} index={1} />
<InputOTPSlot className={slotClassName} index={2} />
</InputOTPGroup>
{showSeparator ? (
<InputOTPSeparator className={separatorClassName} />
) : null}
<InputOTPGroup className={groupClassName}>
<InputOTPSlot className={slotClassName} index={3} />
<InputOTPSlot className={slotClassName} index={4} />
<InputOTPSlot className={slotClassName} index={5} />
</InputOTPGroup>
</InputOTP>
<div className="min-h-[24px] px-4 pt-2 pb-2">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
Expand Down
101 changes: 51 additions & 50 deletions app/routes/_app+/_auth+/verify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { HoneypotInputs } from 'remix-utils/honeypot/react'
import { z } from 'zod'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { ErrorList, OTPField } from '#app/components/forms.tsx'
import { Spacer } from '#app/components/spacer.tsx'
import { Icon } from '#app/components/ui/icon.tsx'
import { StatusButton } from '#app/components/ui/status-button.tsx'
import { checkHoneypot } from '#app/utils/honeypot.server.ts'
import { useIsPending } from '#app/utils/misc.tsx'
Expand Down Expand Up @@ -53,36 +53,32 @@ export default function VerifyRoute() {
)
const type = parseWithZoddType.success ? parseWithZoddType.data : null

const checkPhoneNumber = (
const headingClasses = 'text-h2 sm:text-h1'
const bodyClasses = 'text-body-md text-muted-foreground mt-4'
const buildHeading = (title: string, description: string) => (
<>
<h1 className="text-h1">Check your texts</h1>
<p className="text-body-md text-muted-foreground mt-3">
We've texted you a code to verify your phone number.
</p>
<h1 className={headingClasses}>{title}</h1>
<p className={bodyClasses}>{description}</p>
</>
)
const checkPhoneNumber = buildHeading(
'Check Your Texts',
"We've texted you a code to verify your phone number",
)

const headings: Record<VerificationTypes, React.ReactNode> = {
onboarding: checkPhoneNumber,
'reset-password': checkPhoneNumber,
'change-phone-number': checkPhoneNumber,
'validate-recipient': (
<>
<h1 className="text-h1">Check your texts</h1>
<p className="text-body-md text-muted-foreground mt-3">
We've texted you a code to verify the phone number you gave us. Please
inform your recipient of what you're up to and ask your recipient to
provide you with that code.
</p>
</>
buildHeading(
'Check Your Texts',
"We've texted you a code to verify the phone number you gave us. Please inform your recipient of what you're up to and ask your recipient to provide you with that code.",
)
),
'2fa': (
<>
<h1 className="text-h1">Check your 2FA app</h1>
<p className="text-body-md text-muted-foreground mt-3">
Please enter your 2FA code to verify your identity.
</p>
</>
'2fa': buildHeading(
'Check Your 2FA App',
'Please enter your 2FA code to verify your identity.',
),
}

Expand Down Expand Up @@ -110,40 +106,41 @@ export default function VerifyRoute() {
})

return (
<main className="container flex flex-col items-center justify-center pt-20 pb-32">
<div className="text-center">
<p className="text-muted-foreground text-xs font-semibold tracking-[0.3em] uppercase">
GratiText
</p>
<main className="container flex flex-col items-center justify-start pb-24 pt-12 sm:pt-16">
<div className="text-center max-w-lg">
{type ? headings[type] : 'Invalid Verification Type'}
</div>

<Spacer size="xs" />

<div className="border-border bg-card mt-8 w-full max-w-md rounded-[32px] border px-6 py-8 shadow-sm">
<ErrorList errors={form.errors} id={form.errorId} />
<Form method="POST" {...getFormProps(form)} className="space-y-6">
<div className="mt-10 w-full max-w-md">
<Form method="POST" {...getFormProps(form)} className="space-y-8">
<HoneypotInputs />
<div className="flex items-center justify-center">
<OTPField
type="digits-and-characters"
labelProps={{
htmlFor: fields[codeQueryParam].id,
children: 'Verification Code',
}}
inputProps={{
...getInputProps(fields[codeQueryParam], { type: 'text' }),
autoComplete: 'one-time-code',
autoFocus: true,
}}
errors={fields[codeQueryParam].errors}
/>
</div>
<div className="text-body-xs text-muted-foreground text-center">
<span>Didn't get it? </span>
<ErrorList errors={form.errors} id={form.errorId} />
<OTPField
type="digits-and-characters"
className="w-full"
labelProps={{
htmlFor: fields[codeQueryParam].id,
children: 'Verification Code',
className:
'text-body-sm font-semibold tracking-normal normal-case text-foreground block mb-3',
}}
inputProps={{
...getInputProps(fields[codeQueryParam], { type: 'text' }),
autoComplete: 'one-time-code',
autoFocus: true,
containerClassName:
'justify-center gap-2 sm:justify-start sm:gap-3',
}}
errors={fields[codeQueryParam].errors}
groupClassName="gap-2 sm:gap-3"
showSeparator={false}
slotClassName="h-11 w-11 rounded-full bg-white text-base font-semibold shadow-none dark:bg-[hsl(var(--palette-navy))] sm:h-14 sm:w-14 sm:text-lg"
/>
<div className="text-body-sm text-muted-foreground flex flex-wrap items-center gap-1">
<span>No text after 5 minutes?</span>
<Link
to={type ? resendRoutes[type] : '.'}
className="text-foreground font-semibold underline"
className="text-foreground font-semibold hover:text-foreground/90"
>
Resend the Code
</Link>
Expand All @@ -160,12 +157,16 @@ export default function VerifyRoute() {
})}
/>
<StatusButton
size="lg"
className="w-full bg-[hsl(var(--palette-green-500))] text-[hsl(var(--palette-cream))] hover:bg-[hsl(var(--palette-green-700))]"
status={isPending ? 'pending' : (form.status ?? 'idle')}
type="submit"
disabled={isPending}
>
Continue
<span className="inline-flex items-center gap-3">
Continue
<Icon name="arrow-right" size="sm" aria-hidden="true" />
</span>
</StatusButton>
</Form>
</div>
Expand Down
Loading