Recovery Contact Cooldown

Makes a pending recovery email or phone change visible during its hold, so the real owner can cancel a takeover attempt in time.

StopsHelps the owner catch a swapped recovery contact before it takes effect

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/recovery-contact-cooldown.json

The files land in your project. They're yours to edit.

Usage

recovery-contact-cooldown-usage.tsx
import { RecoveryContactCooldown } from "@/components/ui/recovery-contact-cooldown"

<RecoveryContactCooldown
  key={run}
  previous="tushar.k@gmail.com"
  next="recover.acct77@proton.me"
  requestedAt={requestedAt}
  onCancel={() => new Promise((resolve) => setTimeout(resolve, 600))}
  onReport={() => {}}
/>

What it catches

  • A recovery email or phone change that hasn't taken effect yet
  • Masked old and new contacts so the owner can recognise them
  • Time left, turning urgent in the final hour
  • One-tap cancel and a "this wasn't me" path
  • Says the old contact was notified only when previousNotified is true

Props

RecoveryContactCooldown props
PropTypeDefault
kind

Which recovery method is changing.

"email" | "phone""email"
previous

The current contact, which keeps working during the hold.

string—
next* (required)

The contact it is changing to.

string—
previousNotified

Whether the old contact was sent a notice about the change. Turn off if your backend doesn't send one, so the card never claims it did.

booleantrue
mask

Mask both contacts (t•••r@gmail.com).

booleantrue
requestedAt

When the change was requested.

DateInput—
effectiveAt

When it takes effect. Defaults to requestedAt + delayHours.

DateInput—
delayHours

Length of the hold in hours, when effectiveAt isn't given.

number48
now

A fixed clock, for tests and server rendering. Ticks every second when omitted.

DateInput—
status

Controlled status. Pending turns into applied by itself when the time is up.

RecoveryCooldownStatus—
defaultStatus

Starting status when uncontrolled.

RecoveryCooldownStatus"pending"
onStatusChange

Called when the status changes (cancelled, applied).

(status: RecoveryCooldownStatus) => void—
onCancel

Cancels the change on the server. May return a Promise; a rejection shows an error.

() => void | Promise<void>—
onReport

"This wasn't me": start securing the account (new password, sign out everywhere).

() => void—
onElapsed

Called once when the hold ends and the change takes effect.

() => void—
formatWhen

Formats the effective time.

(date: Date) => string(date) => date.toLocaleString(undefined, { weekday: "shor…
messages

Override any visible or announced text.

Partial<RecoveryCooldownMessages>—

* required. Also accepts the native props of <section> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Section labelled by its title; countdown is role=timer and is not announced every second
  • Only the outcome (applied) is announced via a polite sr-only region
  • Cancel errors show role=alert text
  • After cancel or apply, focus moves to the result message
  • Urgency shown with text and colour

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: The backend must hold the change, keep the old contact working, notify it, and accept cancellation; this card only surfaces that state.

  • Displays the hold; it does not create or enforce it
  • Pending turns to applied on the client clock unless you pass now/status from the server
  • Masking is cosmetic; the full values still reach the browser

What it stops

Helps the owner catch a swapped recovery contact before it takes effect

Also known as

recovery email · recovery phone · security hold · account recovery · change email · mobile number · +91 · this wasn't me

Field type

Dependencies

Pairs well with