# Phone Module

<WebSDK2 />

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

> Follows the [form-based pattern](doc:web-sdk-2-module-patterns#1-form-based-modules) (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.

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

## Properties

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

| Property   | Type                      | Required | Description                        |
| ---------- | ------------------------- | -------- | ---------------------------------- |
| `config`   | `PhoneConfig`             | ✅        | Configuration options              |
| `onFinish` | `() => void`              | ❌        | Called when verification completes |
| `onError`  | `(error: string) => void` | ❌        | Called when an error occurs        |

## Usage

### Vanilla HTML / TypeScript

```html
<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](doc:web-sdk-2-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](doc:web-sdk-2-framework-integration#react-19-shortcut).

```tsx
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](doc:web-sdk-2-framework-integration).

***

## Headless Mode

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

### Quick Start

```tsx
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

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

### States Reference

| Status              | Description                                 | Key Properties                                                                                      |
| ------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `idle`              | Initial state, waiting for `load()`         | –                                                                                                   |
| `loadingPrefill`    | Fetching pre-filled phone from backend      | –                                                                                                   |
| `inputting`         | Ready for phone input                       | `countryCode`, `phonePrefix`, `prefilledPhone?`, `phoneError?`, `otpVerification?`, `optinEnabled?` |
| `submitting`        | Submitting phone number to backend          | –                                                                                                   |
| `sendingInitialOtp` | Sending OTP code via SMS for the first time | –                                                                                                   |
| `resendingOtp`      | Resending OTP code via SMS                  | –                                                                                                   |
| `awaitingOtp`       | Waiting for user to enter OTP               | `resendTimer`, `canResend`, `attemptsRemaining`                                                     |
| `verifyingOtp`      | Verifying OTP code with backend             | `resendTimer`, `canResend`                                                                          |
| `otpError`          | OTP verification failed                     | `otpError`, `attemptsRemaining`, `resendTimer`, `canResend`                                         |
| `finished`          | Verification complete                       | –                                                                                                   |
| `error`             | Fatal error occurred                        | `error`                                                                                             |

### State Properties

**When `status === 'inputting'`:**

| Property         | Type      | Description                                     |
| ---------------- | --------- | ----------------------------------------------- |
| `countryCode`    | `string`  | ISO country code (e.g., `'US'`, `'MX'`)         |
| `phonePrefix`    | `string`  | International dialing prefix (e.g., `'+1'`)     |
| `prefilledPhone` | `string?` | Pre-populated phone number (if `prefill: true`) |
| `phoneError`     | `string?` | Validation error from server                    |

**When `status === 'awaitingOtp'`:**

| Property            | Type      | Description                             |
| ------------------- | --------- | --------------------------------------- |
| `resendTimer`       | `number`  | Seconds remaining before resend allowed |
| `canResend`         | `boolean` | Whether resend button should be enabled |
| `attemptsRemaining` | `number`  | OTP verification attempts left          |

**When `status === 'otpError'`:**

| Property            | Type      | Description                             |
| ------------------- | --------- | --------------------------------------- |
| `otpError`          | `string`  | Error message describing failure        |
| `attemptsRemaining` | `number`  | Remaining attempts before lockout       |
| `resendTimer`       | `number`  | Seconds remaining before resend allowed |
| `canResend`         | `boolean` | Whether resend button should be enabled |

### API Methods

| Method                           | Description                            | When to Use                           |
| -------------------------------- | -------------------------------------- | ------------------------------------- |
| `load()`                         | Initializes the flow                   | Always call first                     |
| `setPhoneNumber(phone, isValid)` | Sets phone number and validation state | When `inputting`, before `submit()`   |
| `setOptInGranted(granted)`       | Sets marketing opt-in preference       | When `inputting`, if opt-in enabled   |
| `submit()`                       | Submits the phone number               | After setting valid phone             |
| `setOtpCode(code)`               | Sets OTP without submitting            | When `awaitingOtp` (controlled input) |
| `submitOtp(code)`                | Sets and submits OTP code              | When `awaitingOtp` or `otpError`      |
| `resendOtp()`                    | Requests new OTP code                  | When `canResend` is `true`            |
| `back()`                         | Returns to phone input                 | When `awaitingOtp`                    |
| `reset()`                        | Resets to initial state                | After `finished` or `error`           |
| `stop()`                         | Cleanup resources                      | When unmounting                       |
| `getState()`                     | Returns current state synchronously    | Anytime                               |
| `subscribe(callback)`            | Subscribe to state changes             | Returns unsubscribe function          |

### React Example

```tsx
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:

| Option                   | Type      | Required | Description                                                                                                 |
| ------------------------ | --------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `otpVerification`        | `boolean` | ✅        | Require OTP (SMS code) verification. If `false`, the phone is verified immediately after submission.        |
| `otpExpirationInMinutes` | `number`  | ✅        | How long the OTP code remains valid. After expiration the user must request a new code.                     |
| `prefill`                | `boolean` | ✅        | Pre-populate with the user's previously stored phone number. Useful for returning users.                    |
| `isInstantVerify`        | `boolean` | ❌        | Use carrier-based instant verification instead of OTP. Not available in all regions. Default `false`.       |
| `optinEnabled`           | `boolean` | ❌        | Show a marketing opt-in checkbox. The user's preference is sent with the phone submission. Default `false`. |
| `maxOtpAttempts`         | `number`  | ❌        | Maximum 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 blacklisted
* Corporate or VoIP numbers may not be accepted

***

## See Also

* [Headless Mode](doc:web-sdk-2-headless-mode): Complete headless API reference
* [Individual Modules](doc:web-sdk-2-individual-modules): Overview of all modules
