Phone Module

The Phone module verifies a user's phone number via SMS OTP (One-Time Password).

📘

This guide is specific to Web SDK 2.0. If you are still using 1.x, you can find documentation here. Contact your Incode Representative for upgrade information and check if you are a candidate for this upgrade.

Full rollout to all clients still TBD.


The Phone module verifies a user's phone number via SMS OTP (One-Time Password).

Follows the form-based pattern (with the OTP sub-loop). See that page for the shared manager lifecycle and skeleton; the rest of this page covers Phone-specific config, states, and methods.

Tag

<incode-phone> is a standard Web Component. Importing the UI subpath registers the custom element; importing the CSS applies the module's styles.

import '@incodetech/web/phone';
import '@incodetech/web/phone/styles.css';

Properties

Set these as JavaScript properties on the element (not as HTML attributes):

PropertyTypeRequiredDescription
configPhoneConfigConfiguration options
onFinish() => voidCalled when verification completes
onError(error: string) => voidCalled when an error occurs

Usage

Vanilla HTML / TypeScript

<incode-phone></incode-phone>

<script type="module">
  import { setup } from '@incodetech/core';
  import '@incodetech/web/phone';
  import '@incodetech/web/phone/styles.css';

  await setup({
    apiURL: 'https://demo-api.incodesmile.com',
    token: 'your-session-token',
  });

  const phone = document.querySelector('incode-phone');
  phone.config = {
    otpVerification: true,
    otpExpirationInMinutes: 5,
    prefill: false,
  };
  phone.onFinish = () => console.log('Phone verified!');
  phone.onError = (err) => console.error('Phone error:', err);
</script>

React

React 18 or earlier: add the one-time JSX augmentation from Framework Integration → TypeScript: JSX support for incode-* tags. React 19+ doesn't need it, and can also use the simpler form from Framework Integration → React 19+ shortcut.

import { useEffect, useRef } from 'react';
import { setup } from '@incodetech/core';
import type { PhoneConfig } from '@incodetech/core/phone';
import '@incodetech/web/phone';
import '@incodetech/web/phone/styles.css';

type PhoneElement = HTMLElement & {
  config: PhoneConfig;
  onFinish: () => void;
  onError: (error: string) => void;
};

await setup({
  apiURL: 'https://demo-api.incodesmile.com',
  token: 'your-session-token',
});

export function PhoneVerification() {
  const ref = useRef<PhoneElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    el.config = {
      otpVerification: true,
      otpExpirationInMinutes: 5,
      prefill: false,
    };
    el.onFinish = () => console.log('Phone verified!');
    el.onError = (err) => console.error('Phone error:', err);
  }, []);

  return <incode-phone ref={ref} />;
}

For Angular (CUSTOM_ELEMENTS_SCHEMA) and Vue (compilerOptions.isCustomElement) setup, see Framework Integration.


Headless Mode

For complete UI control, use the createPhoneManager from @incodetech/core/phone.

Quick Start

import { setup } from '@incodetech/core';
import { createPhoneManager } from '@incodetech/core/phone';

// Initialize SDK first
await setup({
  apiURL: 'https://demo-api.incodesmile.com',
  token: 'your-session-token',
});

const manager = createPhoneManager({
  config: {
    otpVerification: true,
    otpExpirationInMinutes: 5,
    prefill: false,
  },
});

manager.subscribe((state) => {
  console.log('Status:', state.status);

  if (state.status === 'finished') {
    console.log('Phone verified!');
    manager.stop();
  }
});

// Start the flow
manager.load();

// When state is 'inputting', set the phone number and submit
manager.setPhoneNumber('+14155551234', true);
manager.submit();

// When state is 'awaitingOtp', submit the OTP code
manager.submitOtp('ABC123');

State Machine Flow

flowchart LR
    idle -->|load| inputting
    inputting -->|submit| awaitingOtp
    awaitingOtp -->|submitOtp| finished
    awaitingOtp -.->|back| inputting
    awaitingOtp -.->|resendOtp| awaitingOtp

States Reference

StatusDescriptionKey Properties
idleInitial state, waiting for load()
loadingPrefillFetching pre-filled phone from backend
loadingStartInfoFetching start info (ipIsoCode, phonePrefix) before the field is interactiveprefilledPhone?, otpVerification?, optinEnabled?
inputtingReady for phone inputcountryCode, phonePrefix, prefilledPhone?, phoneError?, validationErrors?, isValid, otpVerification?, optinEnabled?
submittingSubmitting phone number to backend
sendingInitialOtpSending OTP code via SMS for the first time
resendingOtpResending OTP code via SMSotpCode, otpLength
awaitingOtpWaiting for user to enter OTPresendTimer, canResend, attemptsRemaining, otpCode, otpLength, otpValidationErrors?, isOtpValid
verifyingOtpVerifying OTP code with backendresendTimer, canResend, otpCode, otpLength
otpErrorOTP verification failedotpError, attemptsRemaining, resendTimer, canResend, otpCode, otpLength, otpValidationErrors?, isOtpValid
finishedVerification complete
errorFatal error occurrederror

State Properties

When status === 'inputting':

PropertyTypeDescription
countryCodestringISO country code (e.g., 'US', 'MX')
phonePrefixstringInternational dialing prefix (e.g., '+1')
prefilledPhonestring?Pre-populated phone number (if prefill: true)
phoneErrorstring?Submission error message if the backend rejected the phone number
validationErrorsPhoneValidationErrors?Client-side validation errors (e.g. invalid/empty phone), surfaced on blur and submit
isValidbooleanTrue iff validationErrors is empty. Optimistic — starts true on initial render.

When status === 'awaitingOtp', 'verifyingOtp', 'otpError', or 'resendingOtp':

PropertyTypeDescription
otpCodestringThe OTP code entered so far (controlled-input value)
otpLengthnumberExpected OTP length (owned by Core — don't hardcode it)
resendTimernumberSeconds remaining before resend allowed (awaitingOtp, verifyingOtp, otpError)
canResendbooleanWhether resend button should be enabled (awaitingOtp, verifyingOtp, otpError)
attemptsRemainingnumberOTP verification attempts left (awaitingOtp, otpError)
otpValidationErrorsOtpValidationErrors?Client-side OTP errors (e.g. incomplete code), surfaced on blur and submit (awaitingOtp, otpError)
isOtpValidbooleanTrue iff otpValidationErrors is empty (awaitingOtp, otpError)
otpErrorstringError message describing failure (otpError only)

API Methods

MethodDescriptionWhen to Use
load()Initializes the flowAlways call first
setPhoneNumber(phone, isValid)Sets phone number and validation stateWhen inputting, before submit()
validatePhone()Runs blur-style validation, populating or clearing validationErrors.phoneWhen inputting
setOptInGranted(granted)Sets marketing opt-in preferenceWhen inputting, if opt-in enabled
submit()Submits the phone numberAfter setting valid phone
setOtpCode(code)Sets OTP without submittingWhen awaitingOtp (controlled input)
validateOtp()Runs blur-style OTP validation, populating or clearing otpValidationErrors.otpWhen awaitingOtp or otpError
submitOtp(code)Sets and submits OTP codeWhen awaitingOtp or otpError
resendOtp()Requests new OTP codeWhen canResend is true
back()Returns to phone inputWhen awaitingOtp
reset()Resets to initial stateAfter finished or error
stop()Cleanup resourcesWhen unmounting
getState()Returns current state synchronouslyAnytime
subscribe(callback)Subscribe to state changesReturns unsubscribe function

React Example

import { useState, useEffect } from 'react';
import { createPhoneManager, type PhoneState } from '@incodetech/core/phone';

function CustomPhoneVerification() {
  const [manager] = useState(() =>
    createPhoneManager({
      config: {
        otpVerification: true,
        otpExpirationInMinutes: 5,
        prefill: false,
      },
    }),
  );
  const [state, setState] = useState<PhoneState>({ status: 'idle' });
  const [phone, setPhone] = useState('');
  const [otp, setOtp] = useState('');

  useEffect(() => {
    const unsubscribe = manager.subscribe(setState);
    manager.load();
    return () => {
      unsubscribe();
      manager.stop();
    };
  }, [manager]);

  const handleSubmitPhone = () => {
    // Use a phone validation library like libphonenumber-js
    const isValid = phone.length >= 10;
    manager.setPhoneNumber(phone, isValid);
    manager.submit();
  };

  switch (state.status) {
    case 'inputting':
      return (
        <div>
          <p>
            Country: {state.countryCode} ({state.phonePrefix})
          </p>
          <input
            type="tel"
            value={phone}
            onChange={(e) => setPhone(e.target.value)}
            placeholder="Enter phone number"
          />
          {state.phoneError && <p className="error">{state.phoneError}</p>}
          <button onClick={handleSubmitPhone}>Send OTP</button>
        </div>
      );

    case 'submitting':
    case 'sendingInitialOtp':
    case 'resendingOtp':
      return <div>Sending OTP...</div>;

    case 'awaitingOtp':
      return (
        <div>
          <p>Enter the 6-character code sent to your phone</p>
          <input
            type="text"
            value={otp}
            onChange={(e) => setOtp(e.target.value.toUpperCase())}
            placeholder="ABC123"
            maxLength={6}
          />
          <button onClick={() => manager.submitOtp(otp)}>Verify</button>

          {state.canResend ? (
            <button onClick={() => manager.resendOtp()}>Resend Code</button>
          ) : (
            <p>Resend available in {state.resendTimer}s</p>
          )}

          <button onClick={() => manager.back()}>Change Phone Number</button>
        </div>
      );

    case 'otpError':
      return (
        <div>
          <p className="error">{state.otpError}</p>
          <p>Attempts remaining: {state.attemptsRemaining}</p>
          <input
            type="text"
            value={otp}
            onChange={(e) => setOtp(e.target.value.toUpperCase())}
            maxLength={6}
          />
          <button onClick={() => manager.submitOtp(otp)}>Try Again</button>
        </div>
      );

    case 'verifyingOtp':
      return <div>Verifying...</div>;

    case 'finished':
      return <div>Phone verified successfully!</div>;

    case 'error':
      return <div className="error">Error: {state.error}</div>;

    default:
      return <div>Loading...</div>;
  }
}

Configuration Options

PhoneConfig shape:

OptionTypeRequiredDescription
otpVerificationbooleanRequire OTP (SMS code) verification. If false, the phone is verified immediately after submission.
otpExpirationInMinutesnumberHow long the OTP code remains valid. After expiration the user must request a new code.
prefillbooleanPre-populate with the user's previously stored phone number. Useful for returning users.
isInstantVerifybooleanUse carrier-based instant verification instead of OTP. Not available in all regions. Default false.
optinEnabledbooleanShow a marketing opt-in checkbox. The user's preference is sent with the phone submission. Default false.
maxOtpAttemptsnumberMaximum OTP verification attempts before lockout. Default 3.

Troubleshooting

OTP Not Received

  • Verify the phone number is correct (include country code)
  • Check the phone can receive SMS
  • Some VoIP numbers may not receive SMS
  • Check with your carrier if SMS is blocked

Invalid OTP Error

  • OTPs expire after the configured time (default 5 minutes)
  • Each OTP can only be used once
  • Request a new OTP if the current one expires
  • Check attemptsRemaining before lockout

Phone Number Rejected

  • Ensure the number includes country code
  • Some numbers may be blocked
  • Corporate or VoIP numbers may not be accepted

See Also


Did this page help you?