Verify screen

ProReact Native · Expo

A complete email or SMS code verification screen: OTP cells that shake and clear on a wrong code, a resend button behind a rolling countdown, and a success check that springs in once the code is accepted.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add @goodui-native-pro/verify-screen

Then run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/verify-screen.

Pro screens install from the license-gated @goodui-native-pro registry. Setup: Pro access.

Usage

// app/(auth)/verify.tsx
import { router, useLocalSearchParams } from "expo-router";
import { VerifyScreen } from "@/components/goodui-native/verify-screen/verify-screen";
import { auth } from "@/lib/auth";

export default function VerifyRoute() {
  const { email } = useLocalSearchParams<{ email: string }>();
  return (
    <VerifyScreen
      channel="email"
      destination={email}
      onVerify={(code) => auth.verifyEmail(email, code)}
      onResend={() => auth.resendEmailCode(email)}
      onContinue={() => router.replace("/welcome")}
      onChangeDestination={() => router.back()}
      onBack={router.back}
    />
  );
}

When to use

Use it for
  • After sign-up to confirm an email address, or a phone number by SMS.
  • Passwordless sign-in and two-factor steps that send a numeric code.
Reach for something else
  • Magic links without a code: use empty-state with a 'Check your inbox' message instead.
  • Codes with letters: otp-input accepts digits only.
  • A code field inside another form: use otp-input on its own.

Examples

SMS with a masked number

<VerifyScreen
  channel="sms"
  destination="+1 ••• ••• 0132"
  resendAfter={60}
  onVerify={(code) => api.verifyPhone(code)}
  onResend={() => api.sendPhoneCode()}
  onContinue={next}
/>

API reference

PropTypeDefault
channel
Where the code went. Changes the copy, the icon and the success title. Default `email`.
"email" | "sms""email"
destination
The address or phone number the code was sent to, shown in the copy. Mask it if you like ("+1 ••• ••• 0132").
stringchannel === "email" ? "maya.patel@example.com" : "+1 (415) 555-0132"
length
Number of digits. Default 6.
number6
onVerify
Runs when every digit is in. Return a promise: resolve to accept (the cells ripple green, then the screen turns into a success check), reject to shake, clear and let the person type again.
(code: string) => unknown—
errorText
Message under the cells after a rejected code.
string"That code didn't work. Check it and try again."
onResend
Sends a new code. Return a promise and the Resend button shows pending, "Code sent" or an error itself.
(channel: VerifyChannel) => unknown—
resendAfter
Seconds before Resend unlocks, on mount and after every resend. Default 30.
number30
expiresInMinutes
Minutes the code stays valid, for the copy. Default 10. `0` leaves the sentence out.
number10
onContinue
Continue button on the success state.
() => unknown—
onChangeDestination
"Wrong email? / Wrong number?" link. Hidden when omitted.
() => void—
onBack
Shows a back button in the top bar.
() => void—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • Digits pop into the cells; a wrong code shakes the row and clears it; an accepted code ripples a green wave across the cells (otp-input).
  • 650 ms after acceptance the form gives way to the success check: the circle springs in, the check draws and a ring pulses; Continue fades in under it.
  • The countdown's seconds roll down digit by digit; at zero it gives way to the Resend button.
  • Reduced motion: no wave, shake, roll or spring; the check and Continue appear in place.
  • Haptic: success or error from otp-input as the code is checked, and success from the check as it lands.
  • Haptic: tap, success and error from the Resend and Continue buttons.
  • The countdown ticks once a second against a deadline, so it stays correct after the app was in the background.
  • Cells, digits and the check animate on the UI thread inside the kit components.

Accessibility

  • The headline is a header. The code field is one real TextInput labelled 'Verification code, 6 digits' with one-time-code and SMS autofill (from otp-input).
  • Accepted and rejected codes are announced by otp-input.
  • Resend is a button that reports busy and announces 'Code sent'; the countdown before it is plain text.
  • Change email or Change number is a link with a 44 pt touch target from hitSlop.

Notes for AI

  • Return the verify promise from onVerify: resolve to accept, reject to shake and clear. Do not show your own error text; pass errorText to change it.
  • onResend should return the send promise; the countdown restarts after the button has shown 'Code sent'. resendAfter sets the seconds (default 30).
  • Navigate in onContinue; the screen shows the success state until then.
  • channel switches all copy between email and SMS; pass destination as it should be read, masked if you want.