React

Ditch Manual OTP Entry: A Guide to WebOTP in React

Tired of users fumbling with OTP codes? Learn how to automatically read SMS verification codes in your React app using the WebOTP API for a seamless, secure login.

A

Alex Taylor

A frontend developer passionate about creating seamless user experiences and modern web APIs.

7 min read122 views
7 min read
1,391 words
122 views

Ditch Manual OTP Entry: A Guide to WebOTP in React

We’ve all been there. You’re trying to log into a service on your phone. It sends you a one-time password (OTP) via SMS. You tap the notification, memorize the six digits, switch back to your browser, and frantically type them in before they vanish from your short-term memory. Sometimes you mistype. Sometimes you forget. It’s a tiny, universal moment of digital friction.

What if you could just… skip that whole dance? What if the browser could securely grab that code for you and fill it in, with a single tap? That’s not a far-off dream; it’s a reality today with the WebOTP API. And if you're a React developer, integrating this seamless experience is easier than you think.

Let's dive into how you can eliminate one of the most common annoyances in modern web authentication and give your users an experience that feels like magic.

The Frictionless Future: What is the WebOTP API?

The WebOTP API is a browser standard that bridges the gap between your web application and the user's SMS inbox. It allows your app, running in the browser, to programmatically receive an OTP from an SMS message, but only under very specific and secure conditions.

The goal is simple: improve the user experience for phone number verification without compromising security. Instead of making the user manually copy and paste, the browser can prompt them with a message like "Allow YourApp to use the code from Messages?" One tap, and the code is in your app, ready to be submitted. It’s smooth, fast, and drastically reduces the chance of user error.

How the Magic Happens: Origin-Binding Explained

This might sound a bit concerning at first. Can any website just start reading my text messages? Absolutely not. The security of WebOTP hinges on a clever mechanism called origin-binding.

For the browser to recognize an OTP and associate it with your web app, the SMS message itself must be specially formatted to include your application's origin (your domain name).

The SMS message must end with your domain name preceded by an @ symbol, and the OTP code preceded by a # symbol.

For example, if your app lives at https://my-awesome-app.com and the OTP is 123456, your server must send an SMS that looks something like this:

Advertisement
Your verification code is 123456.

@my-awesome-app.com #123456

When the user's device receives this message, the browser (if it supports WebOTP) does the following:

  1. Parses the Message: It scans the last line for the @domain.com #code format.
  2. Checks the Origin: It compares the domain in the SMS (my-awesome-app.com) with the origin of the website currently active in the foreground.
  3. Delivers the OTP: If they match, and your app has an active listener for an OTP, the browser will surface the permission prompt and, upon approval, deliver the code (123456) to your app.

This origin-binding is what prevents malicious websites from hijacking OTPs intended for other services. It's a simple yet powerful security model.

Getting Your Ducks in a Row: Prerequisites

Before you jump into the code, there are a few essential requirements to keep in mind:

  • HTTPS is Mandatory: Like most modern web APIs that handle sensitive information, WebOTP only works on secure contexts. Your site must be served over HTTPS.
  • The Special SMS Format: This is non-negotiable. You'll need to coordinate with your backend team to ensure your SMS provider is sending messages in the correct @domain #code format.
  • Browser Support: This is the biggest caveat. As of now, WebOTP is primarily supported by Chrome on Android. While this covers a massive portion of mobile web users, it won't work on iOS or desktop browsers. You'll still need a manual fallback for other users.

Let's Get Practical: Implementing WebOTP in React

Alright, let's build this thing. We'll start with a basic implementation in a component and then refactor it into a clean, reusable custom hook.

The Basic Setup: An OTP Form Component

Imagine you have a simple form component that's displayed after the user requests a code.

// OtpForm.js
import React, { useState } from 'react';

function OtpForm() {
  const [otp, setOtp] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Verifying OTP:', otp);
    // ... your verification logic here
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="otp">Enter your code:</label>
      <input
        id="otp"
        type="text"
        inputMode="numeric"
        autoComplete="one-time-code"
        value={otp}
        onChange={(e) => setOtp(e.target.value)}
      />
      <button type="submit">Verify</button>
    </form>
  );
}

export default OtpForm;

Note the autoComplete="one-time-code" attribute. This is a crucial part of the standard and signals to the browser that this input is designed to receive an OTP. This alone can sometimes trigger helpful OS-level suggestions, but it's essential for the WebOTP API to function correctly.

The Core Logic: Listening for the OTP

Now, let's add the WebOTP logic. We'll use a useEffect hook to start listening for the OTP as soon as the component mounts.

// ... imports
import React, { useState, useEffect } from 'react';

function OtpForm() {
  const [otp, setOtp] = useState('');
  const [status, setStatus] = useState('Waiting for OTP...');

  useEffect(() => {
    // Check if the WebOTP API is supported
    if ('OTPCredential' in window) {
      const abortController = new AbortController();

      const retrieveOtp = async () => {
        try {
          setStatus('Waiting for SMS...');
          const otpCredential = await navigator.credentials.get({
            otp: { transport: ['sms'] },
            signal: abortController.signal,
          });

          setOtp(otpCredential.code);
          setStatus('OTP received!');
          // Optionally, auto-submit the form here
          // formRef.current.submit();
        } catch (err) {
          setStatus('Could not retrieve OTP.');
          console.error(err);
        }
      };

      retrieveOtp();

      // Cleanup function to abort the request if the component unmounts
      return () => abortController.abort();
    } else {
      setStatus('WebOTP not supported in this browser.');
    }
  }, []);

  // ... rest of the component JSX
  return (
    <div>
      <p>{status}</p>
      <form onSubmit={...}> ... </form>
    </div>
  );
}

Let's break that down. We use navigator.credentials.get() with an otp object. This tells the browser we're expecting an OTP delivered via SMS. This call returns a promise that only resolves when a correctly formatted SMS arrives. We use an AbortController to cancel the request if the user navigates away, which is crucial for preventing memory leaks and unexpected behavior.

Level Up: Creating a Reusable `useWebOTP` Hook

The `useEffect` logic is great, but it's not very reusable. Let's extract it into a custom hook to make our components cleaner and our logic portable.

// hooks/useWebOTP.js
import { useState, useEffect } from 'react';

export const useWebOTP = () => {
  const [otp, setOtp] = useState('');
  const [status, setStatus] = useState('idle'); // 'idle', 'waiting', 'success', 'error', 'unsupported'
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!('OTPCredential' in window)) {
      setStatus('unsupported');
      return;
    }

    const abortController = new AbortController();
    setStatus('waiting');

    const retrieveOtp = async () => {
      try {
        const otpCredential = await navigator.credentials.get({
          otp: { transport: ['sms'] },
          signal: abortController.signal,
        });

        setOtp(otpCredential.code);
        setStatus('success');
      } catch (err) {
        setError(err);
        setStatus('error');
      }
    };

    retrieveOtp();

    return () => abortController.abort();
  }, []);

  return { otp, status, error };
};

Now, our `OtpForm` component becomes beautifully simple:

// OtpForm.js (with hook)
import React, { useState, useEffect } from 'react';
import { useWebOTP } from '../hooks/useWebOTP';

function OtpForm() {
  const [manualOtp, setManualOtp] = useState('');
  const { otp: autoOtp, status } = useWebOTP();

  // Use the automatically received OTP if available
  const displayOtp = autoOtp || manualOtp;

  useEffect(() => {
    if (autoOtp) {
      // Optionally auto-submit when OTP is received
      console.log('Auto-submitting with OTP:', autoOtp);
    }
  }, [autoOtp]);

  // ... form JSX
  return (
    <div>
      <p>API Status: {status}</p>
      <form>
        <input
          value={displayOtp}
          onChange={(e) => setManualOtp(e.target.value)}
          autoComplete="one-time-code"
          inputMode="numeric"
        />
        <button>Verify</button>
      </form>
    </div>
  );
}

This approach is much cleaner. The component just consumes the hook and reacts to its state, while the complex API interaction is neatly tucked away.

Final Thoughts: A Small Change for a Big Win

Implementing the WebOTP API in your React app is a perfect example of a progressive enhancement. For users on supported browsers (a huge chunk of the mobile web), you provide a significantly better, faster, and more secure verification flow. For everyone else, the traditional manual entry method continues to work as a fallback.

The biggest hurdle is often coordinating the server-side SMS format change, but the payoff in user satisfaction is well worth the effort. By removing that tiny moment of friction, you show your users that you value their time and are committed to building a truly modern, user-centric experience.

So go ahead, give it a try. Your users (and your conversion rates) will thank you for it.

Topics & Tags

You May Also Like