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):
| 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
<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
| 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
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
attemptsRemainingbefore 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: Complete headless API reference
- Individual Modules: Overview of all modules