@siymo/otp-sdk-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # siymo-otp-react-sdk
2
+
3
+ React hooks SDK for inbound OTP client flows, powered by `@siymo/otp-sdk-core`.
4
+
5
+ ## What It Provides
6
+
7
+ - `useInboundCallOtp()` for inbound call OTP sessions
8
+ - `useInboundSmsOtp()` for inbound SMS OTP sessions
9
+ - Build-time or browser default base URL resolution
10
+ - Live WebSocket events for attempts, lock, verification, and expiry
11
+ - Optional long-poll mode if a React client prefers HTTP waiting
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install siymo-otp-react-sdk @siymo/otp-sdk-core react
17
+ ```
18
+
19
+ ## Default Behavior
20
+
21
+ If you do not pass a `baseUrl`, the hooks resolve it like this:
22
+
23
+ 1. Build-time env value from `SIYMO_OTP_REACT_BASE_URL`
24
+ 2. Build-time env value from `SIYMO_OTP_BASE_URL`
25
+ 3. Build-time env value from `OTP_BASE_URL`
26
+ 4. Build-time env value from `baseURL`
27
+ 5. Browser `location.origin`
28
+ 6. Fallback to `http://localhost:3000`
29
+
30
+ That lets you bake a service URL into the built package, while still keeping same-origin React apps working without extra configuration.
31
+
32
+ To build the package with a default service URL:
33
+
34
+ ```bash
35
+ SIYMO_OTP_REACT_BASE_URL=http://localhost:3000 npm run build
36
+ ```
37
+
38
+ ## Basic Usage
39
+
40
+ ```tsx
41
+ import { useInboundSmsOtp } from 'siymo-otp-react-sdk';
42
+
43
+ export function InboundSmsWidget() {
44
+ const { data, error, loading, create, eventLog } = useInboundSmsOtp({
45
+ onTried(event) {
46
+ console.log('Try number:', event.data.attempts);
47
+ },
48
+ onSuccess() {
49
+ console.log('OTP verified');
50
+ },
51
+ });
52
+
53
+ async function handleStart() {
54
+ await create({
55
+ phone: '+998990649000',
56
+ expiration: 60,
57
+ maxTries: 3,
58
+ qrCode: true,
59
+ });
60
+ }
61
+
62
+ return (
63
+ <div>
64
+ <button onClick={handleStart}>Create session</button>
65
+ <p>Loading: {loading ? 'yes' : 'no'}</p>
66
+ <p>Status: {data.status}</p>
67
+ <p>Attempts: {data.attempts}</p>
68
+ <p>Tries left: {data.triesLeft ?? '-'}</p>
69
+ <p>Decoded SMS: {data.decodedText ?? '-'}</p>
70
+ {data.qrCodeImage ? <img src={data.qrCodeImage} alt="Inbound SMS QR code" width={180} /> : null}
71
+ <p>Latest event: {eventLog.at(-1)?.description ?? '-'}</p>
72
+ {error ? <p>{error.message}</p> : null}
73
+ </div>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ## Hook Return Shape
79
+
80
+ Both hooks expose:
81
+
82
+ - `data` as the primary, flattened payload for the hook
83
+ - `loading`, `error`, `message`, and `called` for easy UI state
84
+ - `create(request)`, `restart(request?)`, `wait(sessionId?)`, `stop()`, and `clear()`
85
+ - Legacy aliases are still available: `start`, `waitForSession`, `cancel`, `reset`
86
+ - `eventLog` with pre-described entries ready for rendering
87
+ - Raw escape hatches when needed: `session`, `events`, `lastEvent`, `lastAttemptEvent`, `verifiedEvent`, `lockedEvent`, `expiredEvent`
88
+ - `attempts`, `triesLeft`, `status`, `isWaiting`, `isVerified`, `isLocked`, `isExpired`
89
+
90
+ `useInboundCallOtp()` also exposes `otpCode`, `callHref`, and `qrCodeImage`.
91
+
92
+ `useInboundSmsOtp()` also exposes `decodedText` and `qrCodeImage`.
93
+
94
+ The `data` shape is channel-specific:
95
+
96
+ - Call hook data: `servicePhone`, `callerPhone`, `qrCodeImage`, `otpCode`, `callHref`
97
+ - SMS hook data: `servicePhone`, `senderPhone`, `qrCodeImage`, `rawText`, `decodedText`
98
+
99
+ ## Options
100
+
101
+ ```ts
102
+ type UseInboundCallOtpOptions = {
103
+ autoWait?: boolean;
104
+ waitMode?: 'websocket' | 'long-poll';
105
+ timeoutMs?: number;
106
+ baseUrl?: string;
107
+ websocketBaseUrl?: string;
108
+ client?: SiymoOtpClient;
109
+ clientOptions?: Omit<SiymoOtpClientOptions, 'baseUrl' | 'websocketBaseUrl'>;
110
+ onCreated?: (session, result) => void;
111
+ onSubscribed?: (event, result) => void;
112
+ onTried?: (event, result) => void;
113
+ onSuccess?: (event, result) => void;
114
+ onLocked?: (event, result) => void;
115
+ onExpired?: (event, result) => void;
116
+ onError?: (error, result) => void;
117
+ onStatusChange?: (status, result) => void;
118
+ };
119
+ ```
120
+
121
+ Notes:
122
+
123
+ - `waitMode` defaults to `websocket`
124
+ - `autoWait` defaults to `true`
125
+ - `baseUrl` still overrides any build-time default when you want a per-hook override
126
+ - Pass `qrCode: true` in `create()` when you want the API to return a scannable `qrCodeImage` data URL
127
+ - WebSocket mode is the only mode that can stream `otp.attempt` events live
128
+ - Long-poll mode still updates verified / locked / expired state, but it cannot stream intermediate attempts
129
+ - Callback handlers receive the latest hook result, including the derived `data` object
130
+
131
+ ## Examples
132
+
133
+ The package includes two basic React examples:
134
+
135
+ - [`examples/inbound-call-client`](./examples/inbound-call-client)
136
+ - [`examples/inbound-sms-client`](./examples/inbound-sms-client)
137
+
138
+ Each example consumes this React package plus the core `@siymo/otp-sdk-core`.
@@ -0,0 +1,2 @@
1
+ export declare const BUILD_TIME_SIYMO_OTP_REACT_BASE_URL: string | undefined;
2
+ export declare const BUILD_TIME_SIYMO_OTP_REACT_BASE_URL_ENV_VARS: readonly ["SIYMO_OTP_REACT_BASE_URL", "SIYMO_OTP_BASE_URL", "OTP_BASE_URL", "baseURL"];
@@ -0,0 +1,8 @@
1
+ // This file is updated by `npm run build` to capture any build-time base URL.
2
+ export const BUILD_TIME_SIYMO_OTP_REACT_BASE_URL = "https://otp.inadullaev.com";
3
+ export const BUILD_TIME_SIYMO_OTP_REACT_BASE_URL_ENV_VARS = [
4
+ "SIYMO_OTP_REACT_BASE_URL",
5
+ "SIYMO_OTP_BASE_URL",
6
+ "OTP_BASE_URL",
7
+ "baseURL"
8
+ ];
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_SIYMO_OTP_REACT_BASE_URL = "http://localhost:3000";
2
+ export declare function resolveDefaultSiymoOtpBaseUrl(): string;
@@ -0,0 +1,16 @@
1
+ import { BUILD_TIME_SIYMO_OTP_REACT_BASE_URL } from './buildConfig.js';
2
+ export const DEFAULT_SIYMO_OTP_REACT_BASE_URL = 'http://localhost:3000';
3
+ export function resolveDefaultSiymoOtpBaseUrl() {
4
+ const configuredBaseUrl = BUILD_TIME_SIYMO_OTP_REACT_BASE_URL?.trim();
5
+ if (configuredBaseUrl && configuredBaseUrl.length > 0) {
6
+ return configuredBaseUrl;
7
+ }
8
+ const origin = globalThis.location?.origin;
9
+ if (typeof origin === 'string') {
10
+ const trimmed = origin.trim();
11
+ if (trimmed.length > 0 && trimmed !== 'null') {
12
+ return trimmed;
13
+ }
14
+ }
15
+ return DEFAULT_SIYMO_OTP_REACT_BASE_URL;
16
+ }
@@ -0,0 +1,6 @@
1
+ export { SiymoOtpApiError, SiymoOtpClient, SiymoOtpConfigurationError, SiymoOtpError, SiymoOtpWebSocketError, } from '@siymo/otp-sdk-core';
2
+ export type { InboundReceiptType, InitiateInboundCallResponse, InitiateInboundRequest, InitiateInboundSmsResponse, OtpAttemptEvent, OtpExpiredEvent, OtpLockedEvent, OtpLongPollFailureResponse, OtpLongPollSuccessResponse, OtpSessionEvent, OtpSubscribedEvent, OtpVerifiedEvent, SiymoOtpClientOptions, } from '@siymo/otp-sdk-core';
3
+ export { DEFAULT_SIYMO_OTP_REACT_BASE_URL, resolveDefaultSiymoOtpBaseUrl, } from './defaults.js';
4
+ export type { InboundCallOtpData, InboundOtpActivityItem, InboundOtpActivityPayload, InboundOtpData, InboundOtpExpiredPayload, InboundOtpHookResult, InboundOtpHookStatus, InboundOtpLockedPayload, InboundOtpSuccessPayload, InboundOtpWaitMode, InboundSmsOtpData, UseInboundCallOtpOptions, UseInboundCallOtpResult, UseInboundSmsOtpOptions, UseInboundSmsOtpResult, UseSiymoOtpHookOptions, } from './types.js';
5
+ export { useInboundCallOtp } from './useInboundCallOtp.js';
6
+ export { useInboundSmsOtp } from './useInboundSmsOtp.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { SiymoOtpApiError, SiymoOtpClient, SiymoOtpConfigurationError, SiymoOtpError, SiymoOtpWebSocketError, } from '@siymo/otp-sdk-core';
2
+ export { DEFAULT_SIYMO_OTP_REACT_BASE_URL, resolveDefaultSiymoOtpBaseUrl, } from './defaults.js';
3
+ export { useInboundCallOtp } from './useInboundCallOtp.js';
4
+ export { useInboundSmsOtp } from './useInboundSmsOtp.js';
@@ -0,0 +1,121 @@
1
+ import type { InitiateInboundCallResponse, InitiateInboundRequest, InitiateInboundSmsResponse, OtpAttemptEvent, OtpExpiredEvent, OtpLockedEvent, OtpLongPollFailureResponse, OtpLongPollSuccessResponse, OtpSessionEvent, OtpSubscribedEvent, OtpVerifiedEvent, SiymoOtpClient, SiymoOtpClientOptions } from '@siymo/otp-sdk-core';
2
+ type InboundOtpSession = InitiateInboundCallResponse | InitiateInboundSmsResponse;
3
+ export type InboundOtpHookStatus = 'idle' | 'starting' | 'pending' | 'verified' | 'locked' | 'expired' | 'error';
4
+ export type InboundOtpWaitMode = 'websocket' | 'long-poll';
5
+ export type InboundOtpSuccessPayload = OtpVerifiedEvent | OtpLongPollSuccessResponse;
6
+ export type InboundOtpLockedPayload = OtpLockedEvent | (OtpLongPollFailureResponse & {
7
+ status: 'LOCKED';
8
+ });
9
+ export type InboundOtpExpiredPayload = OtpExpiredEvent | (OtpLongPollFailureResponse & {
10
+ status: 'EXPIRED';
11
+ });
12
+ export type InboundOtpActivityPayload = OtpSessionEvent | OtpLongPollSuccessResponse | OtpLongPollFailureResponse;
13
+ export interface InboundOtpActivityItem<TPayload extends InboundOtpActivityPayload = InboundOtpActivityPayload> {
14
+ type: 'otp.subscribed' | 'otp.attempt' | 'otp.locked' | 'otp.verified' | 'otp.expired';
15
+ description: string;
16
+ occurredAt: number | string | null;
17
+ payload: TPayload;
18
+ }
19
+ export interface InboundOtpData<TSession extends InboundOtpSession = InboundOtpSession> {
20
+ session: TSession | null;
21
+ sessionId: string | null;
22
+ channel: TSession['channel'] | null;
23
+ direction: TSession['direction'] | null;
24
+ status: InboundOtpHookStatus;
25
+ attempts: number;
26
+ triesLeft: number | null;
27
+ length: number | null;
28
+ expiresIn: number | null;
29
+ maxTries: number | null;
30
+ subscribedExpiresAt: number | null;
31
+ lastEvent: OtpSessionEvent | null;
32
+ lastEventText: string | null;
33
+ }
34
+ export interface InboundCallOtpData extends InboundOtpData<InitiateInboundCallResponse> {
35
+ channel: 'call' | null;
36
+ servicePhone: string | null;
37
+ callerPhone: string | null;
38
+ qrCodeImage: string | null;
39
+ otpCode: string | null;
40
+ callHref: string | null;
41
+ }
42
+ export interface InboundSmsOtpData extends InboundOtpData<InitiateInboundSmsResponse> {
43
+ channel: 'sms' | null;
44
+ servicePhone: string | null;
45
+ senderPhone: string | null;
46
+ qrCodeImage: string | null;
47
+ rawText: string | null;
48
+ decodedText: string | null;
49
+ }
50
+ export interface InboundOtpHookResult<TSession extends InboundOtpSession = InboundOtpSession, TData extends InboundOtpData<TSession> = InboundOtpData<TSession>> {
51
+ data: TData;
52
+ loading: boolean;
53
+ called: boolean;
54
+ hasSession: boolean;
55
+ message: string | null;
56
+ latestEventText: string | null;
57
+ eventLog: InboundOtpActivityItem[];
58
+ lastRequest: InitiateInboundRequest | null;
59
+ waitMode: InboundOtpWaitMode;
60
+ status: InboundOtpHookStatus;
61
+ sessionId: string | null;
62
+ session: TSession | null;
63
+ events: OtpSessionEvent[];
64
+ lastEvent: OtpSessionEvent | null;
65
+ subscribedEvent: OtpSubscribedEvent | null;
66
+ subscribedExpiresAt: number | null;
67
+ lastAttemptEvent: OtpAttemptEvent | null;
68
+ lockedEvent: OtpLockedEvent | null;
69
+ verifiedEvent: OtpVerifiedEvent | null;
70
+ expiredEvent: OtpExpiredEvent | null;
71
+ longPollResult: OtpLongPollSuccessResponse | null;
72
+ longPollFailure: OtpLongPollFailureResponse | null;
73
+ attempts: number;
74
+ triesLeft: number | null;
75
+ error: Error | null;
76
+ isIdle: boolean;
77
+ isStarting: boolean;
78
+ isWaiting: boolean;
79
+ isVerified: boolean;
80
+ isLocked: boolean;
81
+ isExpired: boolean;
82
+ create(request: InitiateInboundRequest): Promise<TSession>;
83
+ restart(request?: InitiateInboundRequest): Promise<TSession>;
84
+ wait(sessionId?: string): Promise<void>;
85
+ stop(): Promise<void>;
86
+ clear(): Promise<void>;
87
+ start(request: InitiateInboundRequest): Promise<TSession>;
88
+ waitForSession(sessionId?: string): Promise<void>;
89
+ cancel(): Promise<void>;
90
+ reset(): Promise<void>;
91
+ }
92
+ export interface UseInboundCallOtpResult extends InboundOtpHookResult<InitiateInboundCallResponse, InboundCallOtpData> {
93
+ otpCode: string | null;
94
+ callHref: string | null;
95
+ qrCodeImage: string | null;
96
+ }
97
+ export interface UseInboundSmsOtpResult extends InboundOtpHookResult<InitiateInboundSmsResponse, InboundSmsOtpData> {
98
+ decodedText: string | null;
99
+ qrCodeImage: string | null;
100
+ }
101
+ export interface UseSiymoOtpHookOptions<TSession extends InboundOtpSession = InboundOtpSession, TData extends InboundOtpData<TSession> = InboundOtpData<TSession>> {
102
+ autoWait?: boolean;
103
+ waitMode?: InboundOtpWaitMode;
104
+ timeoutMs?: number;
105
+ baseUrl?: string;
106
+ websocketBaseUrl?: string;
107
+ client?: SiymoOtpClient;
108
+ clientOptions?: Omit<SiymoOtpClientOptions, 'baseUrl' | 'websocketBaseUrl'>;
109
+ onCreated?: (session: TSession, result: InboundOtpHookResult<TSession, TData>) => void;
110
+ onEvent?: (event: OtpSessionEvent, result: InboundOtpHookResult<TSession, TData>) => void;
111
+ onSubscribed?: (event: OtpSubscribedEvent, result: InboundOtpHookResult<TSession, TData>) => void;
112
+ onTried?: (event: OtpAttemptEvent, result: InboundOtpHookResult<TSession, TData>) => void;
113
+ onLocked?: (event: InboundOtpLockedPayload, result: InboundOtpHookResult<TSession, TData>) => void;
114
+ onSuccess?: (event: InboundOtpSuccessPayload, result: InboundOtpHookResult<TSession, TData>) => void;
115
+ onExpired?: (event: InboundOtpExpiredPayload, result: InboundOtpHookResult<TSession, TData>) => void;
116
+ onError?: (error: Error, result: InboundOtpHookResult<TSession, TData>) => void;
117
+ onStatusChange?: (status: InboundOtpHookStatus, result: InboundOtpHookResult<TSession, TData>) => void;
118
+ }
119
+ export type UseInboundCallOtpOptions = UseSiymoOtpHookOptions<InitiateInboundCallResponse, InboundCallOtpData>;
120
+ export type UseInboundSmsOtpOptions = UseSiymoOtpHookOptions<InitiateInboundSmsResponse, InboundSmsOtpData>;
121
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { UseInboundCallOtpOptions, UseInboundCallOtpResult } from './types.js';
2
+ export declare function useInboundCallOtp(options?: UseInboundCallOtpOptions): UseInboundCallOtpResult;
@@ -0,0 +1,53 @@
1
+ import { useInboundOtpSession } from './useInboundOtp.js';
2
+ export function useInboundCallOtp(options = {}) {
3
+ const result = useInboundOtpSession({
4
+ options,
5
+ initiate(client, request) {
6
+ return client.initiateInboundCall(request);
7
+ },
8
+ selectData(input) {
9
+ const servicePhone = input.session?.phone ??
10
+ input.verifiedEvent?.data.serverPhone ??
11
+ input.lockedEvent?.data.serverPhone ??
12
+ input.longPollResult?.serverPhone ??
13
+ input.longPollFailure?.serverPhone ??
14
+ null;
15
+ const callerPhone = input.session?.from ??
16
+ input.verifiedEvent?.data.phone ??
17
+ input.lockedEvent?.data.phone ??
18
+ input.longPollResult?.phone ??
19
+ input.longPollFailure?.phone ??
20
+ null;
21
+ const otpCode = input.session?.code ?? null;
22
+ return {
23
+ session: input.session,
24
+ sessionId: input.sessionId,
25
+ channel: input.session?.channel ?? null,
26
+ direction: input.session?.direction ?? null,
27
+ status: input.status,
28
+ attempts: input.attempts,
29
+ triesLeft: input.triesLeft,
30
+ length: input.session?.length ?? null,
31
+ expiresIn: input.session?.expiresIn ?? null,
32
+ maxTries: input.session?.maxTries ??
33
+ input.lockedEvent?.data.maxTries ??
34
+ input.longPollFailure?.maxTries ??
35
+ null,
36
+ subscribedExpiresAt: input.subscribedExpiresAt,
37
+ lastEvent: input.lastEvent,
38
+ lastEventText: input.latestEventText,
39
+ servicePhone,
40
+ callerPhone,
41
+ qrCodeImage: input.session?.qrCodeImage ?? null,
42
+ otpCode,
43
+ callHref: servicePhone && otpCode ? `tel:${servicePhone},${otpCode}` : null,
44
+ };
45
+ },
46
+ });
47
+ return {
48
+ ...result,
49
+ otpCode: result.data.otpCode,
50
+ callHref: result.data.callHref,
51
+ qrCodeImage: result.data.qrCodeImage,
52
+ };
53
+ }
@@ -0,0 +1,42 @@
1
+ import { SiymoOtpClient, type InitiateInboundCallResponse, type InitiateInboundRequest, type InitiateInboundSmsResponse, type OtpAttemptEvent, type OtpExpiredEvent, type OtpLockedEvent, type OtpLongPollFailureResponse, type OtpLongPollSuccessResponse, type OtpSessionEvent, type OtpSubscribedEvent, type OtpVerifiedEvent } from '@siymo/otp-sdk-core';
2
+ import type { InboundOtpActivityItem, InboundOtpData, InboundOtpHookResult, InboundOtpHookStatus, UseSiymoOtpHookOptions } from './types.js';
3
+ type InboundOtpSession = InitiateInboundCallResponse | InitiateInboundSmsResponse;
4
+ interface InboundOtpResultInput<TSession extends InboundOtpSession> {
5
+ loading: boolean;
6
+ called: boolean;
7
+ hasSession: boolean;
8
+ message: string | null;
9
+ latestEventText: string | null;
10
+ eventLog: InboundOtpActivityItem[];
11
+ lastRequest: InitiateInboundRequest | null;
12
+ waitMode: 'websocket' | 'long-poll';
13
+ status: InboundOtpHookStatus;
14
+ sessionId: string | null;
15
+ session: TSession | null;
16
+ events: OtpSessionEvent[];
17
+ lastEvent: OtpSessionEvent | null;
18
+ subscribedEvent: OtpSubscribedEvent | null;
19
+ subscribedExpiresAt: number | null;
20
+ lastAttemptEvent: OtpAttemptEvent | null;
21
+ lockedEvent: OtpLockedEvent | null;
22
+ verifiedEvent: OtpVerifiedEvent | null;
23
+ expiredEvent: OtpExpiredEvent | null;
24
+ longPollResult: OtpLongPollSuccessResponse | null;
25
+ longPollFailure: OtpLongPollFailureResponse | null;
26
+ attempts: number;
27
+ triesLeft: number | null;
28
+ error: Error | null;
29
+ isIdle: boolean;
30
+ isStarting: boolean;
31
+ isWaiting: boolean;
32
+ isVerified: boolean;
33
+ isLocked: boolean;
34
+ isExpired: boolean;
35
+ }
36
+ interface UseInboundOtpSessionConfig<TSession extends InboundOtpSession, TData extends InboundOtpData<TSession>> {
37
+ initiate(client: SiymoOtpClient, request: InitiateInboundRequest): Promise<TSession>;
38
+ options: UseSiymoOtpHookOptions<TSession, TData>;
39
+ selectData(input: InboundOtpResultInput<TSession>): TData;
40
+ }
41
+ export declare function useInboundOtpSession<TSession extends InboundOtpSession, TData extends InboundOtpData<TSession>>(config: UseInboundOtpSessionConfig<TSession, TData>): InboundOtpHookResult<TSession, TData>;
42
+ export {};
@@ -0,0 +1,646 @@
1
+ import { useEffect, useRef, useState, } from 'react';
2
+ import { SiymoOtpApiError, SiymoOtpClient, } from '@siymo/otp-sdk-core';
3
+ import { resolveDefaultSiymoOtpBaseUrl } from './defaults.js';
4
+ export function useInboundOtpSession(config) {
5
+ const waitMode = config.options.waitMode ?? 'websocket';
6
+ const [state, setState] = useState(() => createInitialState());
7
+ const stateRef = useRef(state);
8
+ const previousStateRef = useRef(state);
9
+ const callbacksRef = useRef(config.options);
10
+ const activeWaitRef = useRef(null);
11
+ const lastRequestRef = useRef(null);
12
+ const nextActiveWaitIdRef = useRef(0);
13
+ callbacksRef.current = config.options;
14
+ const updateState = (updater) => {
15
+ setState((current) => {
16
+ const next = typeof updater === 'function' ? updater(current) : updater;
17
+ stateRef.current = next;
18
+ return next;
19
+ });
20
+ };
21
+ useEffect(() => {
22
+ return () => {
23
+ void stopActiveWait(activeWaitRef);
24
+ };
25
+ }, []);
26
+ async function start(request) {
27
+ lastRequestRef.current = { ...request };
28
+ await stopActiveWait(activeWaitRef);
29
+ updateState({
30
+ ...createInitialState(),
31
+ status: 'starting',
32
+ });
33
+ const client = resolveClient(config.options);
34
+ const session = await config.initiate(client, request).catch((error) => {
35
+ const normalized = normalizeError(error);
36
+ updateState({
37
+ ...createInitialState(),
38
+ status: 'error',
39
+ error: normalized,
40
+ });
41
+ throw normalized;
42
+ });
43
+ updateState({
44
+ ...createInitialState(),
45
+ status: 'pending',
46
+ session,
47
+ triesLeft: session.maxTries,
48
+ });
49
+ if (config.options.autoWait ?? true) {
50
+ try {
51
+ await waitForSession(session.sessionId);
52
+ }
53
+ catch (error) {
54
+ const normalized = normalizeError(error);
55
+ updateState((current) => ({
56
+ ...current,
57
+ status: 'error',
58
+ error: normalized,
59
+ isWaiting: false,
60
+ }));
61
+ throw normalized;
62
+ }
63
+ }
64
+ return session;
65
+ }
66
+ async function restart(request) {
67
+ const nextRequest = request ?? lastRequestRef.current;
68
+ if (!nextRequest) {
69
+ throw new Error('No previous inbound OTP request is available to restart.');
70
+ }
71
+ return start(nextRequest);
72
+ }
73
+ async function waitForSession(sessionId) {
74
+ const targetSessionId = sessionId ?? stateRef.current.session?.sessionId;
75
+ if (!targetSessionId) {
76
+ throw new Error('No inbound OTP session is available to wait for.');
77
+ }
78
+ await stopActiveWait(activeWaitRef);
79
+ const activeWait = {
80
+ id: nextActiveWaitIdRef.current + 1,
81
+ controller: new AbortController(),
82
+ };
83
+ nextActiveWaitIdRef.current = activeWait.id;
84
+ activeWaitRef.current = activeWait;
85
+ updateState((current) => ({
86
+ ...current,
87
+ status: 'pending',
88
+ isWaiting: true,
89
+ error: null,
90
+ longPollResult: null,
91
+ longPollFailure: null,
92
+ }));
93
+ const client = resolveClient(config.options);
94
+ scheduleWaitTimeout(activeWaitRef, activeWait, config.options.timeoutMs, targetSessionId, updateState);
95
+ if (waitMode === 'long-poll') {
96
+ void client
97
+ .waitForConfirmationLongPoll(targetSessionId, {
98
+ signal: activeWait.controller.signal,
99
+ })
100
+ .then((result) => {
101
+ if (!isCurrentActiveWait(activeWaitRef, activeWait)) {
102
+ return;
103
+ }
104
+ finalizeActiveWait(activeWaitRef, activeWait);
105
+ updateState((current) => ({
106
+ ...current,
107
+ status: 'verified',
108
+ longPollResult: result,
109
+ error: null,
110
+ isWaiting: false,
111
+ }));
112
+ })
113
+ .catch((error) => {
114
+ if (!isCurrentActiveWait(activeWaitRef, activeWait)) {
115
+ return;
116
+ }
117
+ finalizeActiveWait(activeWaitRef, activeWait);
118
+ if (isAbortLikeError(error)) {
119
+ updateState((current) => ({
120
+ ...current,
121
+ isWaiting: false,
122
+ }));
123
+ return;
124
+ }
125
+ const failure = extractLongPollFailure(error);
126
+ if (failure?.status === 'LOCKED') {
127
+ updateState((current) => ({
128
+ ...current,
129
+ status: 'locked',
130
+ longPollFailure: failure,
131
+ attempts: failure.attempts ?? current.attempts,
132
+ triesLeft: failure.triesLeft ?? current.triesLeft,
133
+ error: null,
134
+ isWaiting: false,
135
+ }));
136
+ return;
137
+ }
138
+ if (failure?.status === 'EXPIRED') {
139
+ updateState((current) => ({
140
+ ...current,
141
+ status: 'expired',
142
+ longPollFailure: failure,
143
+ error: null,
144
+ isWaiting: false,
145
+ }));
146
+ return;
147
+ }
148
+ updateState((current) => ({
149
+ ...current,
150
+ status: 'error',
151
+ error: normalizeError(error),
152
+ isWaiting: false,
153
+ }));
154
+ });
155
+ return;
156
+ }
157
+ try {
158
+ activeWait.subscription = client.subscribeToSession(targetSessionId, {
159
+ signal: activeWait.controller.signal,
160
+ onSubscribed: (event) => {
161
+ if (!isCurrentActiveWait(activeWaitRef, activeWait)) {
162
+ return;
163
+ }
164
+ updateState((current) => recordEvent(current, event, {
165
+ subscribedEvent: event,
166
+ subscribedExpiresAt: event.data.expiresAt,
167
+ }));
168
+ },
169
+ onAttempt: (event) => {
170
+ if (!isCurrentActiveWait(activeWaitRef, activeWait)) {
171
+ return;
172
+ }
173
+ updateState((current) => recordEvent(current, event, {
174
+ attempts: event.data.attempts,
175
+ triesLeft: event.data.triesLeft,
176
+ lastAttemptEvent: event,
177
+ }));
178
+ },
179
+ onLocked: (event) => {
180
+ if (!isCurrentActiveWait(activeWaitRef, activeWait)) {
181
+ return;
182
+ }
183
+ finalizeActiveWait(activeWaitRef, activeWait);
184
+ updateState((current) => recordEvent(current, event, {
185
+ status: 'locked',
186
+ attempts: event.data.attempts,
187
+ triesLeft: event.data.triesLeft,
188
+ lockedEvent: event,
189
+ isWaiting: false,
190
+ }));
191
+ },
192
+ onVerified: (event) => {
193
+ if (!isCurrentActiveWait(activeWaitRef, activeWait)) {
194
+ return;
195
+ }
196
+ finalizeActiveWait(activeWaitRef, activeWait);
197
+ updateState((current) => recordEvent(current, event, {
198
+ status: 'verified',
199
+ verifiedEvent: event,
200
+ isWaiting: false,
201
+ }));
202
+ },
203
+ onExpired: (event) => {
204
+ if (!isCurrentActiveWait(activeWaitRef, activeWait)) {
205
+ return;
206
+ }
207
+ finalizeActiveWait(activeWaitRef, activeWait);
208
+ updateState((current) => recordEvent(current, event, {
209
+ status: 'expired',
210
+ expiredEvent: event,
211
+ isWaiting: false,
212
+ }));
213
+ },
214
+ onError: (error) => {
215
+ if (!isCurrentActiveWait(activeWaitRef, activeWait)) {
216
+ return;
217
+ }
218
+ finalizeActiveWait(activeWaitRef, activeWait);
219
+ updateState((current) => ({
220
+ ...current,
221
+ status: 'error',
222
+ error: normalizeError(error),
223
+ isWaiting: false,
224
+ }));
225
+ },
226
+ onClose: () => {
227
+ if (!isCurrentActiveWait(activeWaitRef, activeWait)) {
228
+ return;
229
+ }
230
+ finalizeActiveWait(activeWaitRef, activeWait);
231
+ if (activeWait.controller.signal.aborted) {
232
+ updateState((current) => ({
233
+ ...current,
234
+ isWaiting: false,
235
+ }));
236
+ return;
237
+ }
238
+ updateState((current) => ({
239
+ ...current,
240
+ status: current.status === 'pending' || current.status === 'starting'
241
+ ? 'error'
242
+ : current.status,
243
+ error: current.status === 'pending' || current.status === 'starting'
244
+ ? new Error(`WebSocket closed before session ${targetSessionId} completed.`)
245
+ : current.error,
246
+ isWaiting: false,
247
+ }));
248
+ },
249
+ });
250
+ }
251
+ catch (error) {
252
+ finalizeActiveWait(activeWaitRef, activeWait);
253
+ const normalized = normalizeError(error);
254
+ updateState((current) => ({
255
+ ...current,
256
+ status: 'error',
257
+ error: normalized,
258
+ isWaiting: false,
259
+ }));
260
+ throw normalized;
261
+ }
262
+ }
263
+ async function cancel() {
264
+ await stopActiveWait(activeWaitRef);
265
+ updateState((current) => ({
266
+ ...current,
267
+ isWaiting: false,
268
+ error: null,
269
+ }));
270
+ }
271
+ async function reset() {
272
+ await stopActiveWait(activeWaitRef);
273
+ updateState(createInitialState());
274
+ }
275
+ const resultInput = createResultInput(state, waitMode, lastRequestRef.current);
276
+ const result = createHookResult(resultInput, config.selectData(resultInput), {
277
+ start,
278
+ restart,
279
+ waitForSession,
280
+ cancel,
281
+ reset,
282
+ });
283
+ const resultRef = useRef(result);
284
+ resultRef.current = result;
285
+ useEffect(() => {
286
+ const previousState = previousStateRef.current;
287
+ const callbacks = callbacksRef.current;
288
+ const currentResult = resultRef.current;
289
+ if (state.session !== previousState.session && state.session) {
290
+ callbacks.onCreated?.(state.session, currentResult);
291
+ }
292
+ if (state.lastEvent !== previousState.lastEvent && state.lastEvent) {
293
+ callbacks.onEvent?.(state.lastEvent, currentResult);
294
+ switch (state.lastEvent.event) {
295
+ case 'otp.subscribed':
296
+ callbacks.onSubscribed?.(state.lastEvent, currentResult);
297
+ break;
298
+ case 'otp.attempt':
299
+ callbacks.onTried?.(state.lastEvent, currentResult);
300
+ break;
301
+ case 'otp.locked':
302
+ callbacks.onLocked?.(state.lastEvent, currentResult);
303
+ break;
304
+ case 'otp.verified':
305
+ callbacks.onSuccess?.(state.lastEvent, currentResult);
306
+ break;
307
+ case 'otp.expired':
308
+ callbacks.onExpired?.(state.lastEvent, currentResult);
309
+ break;
310
+ }
311
+ }
312
+ if (state.longPollResult !== previousState.longPollResult && state.longPollResult) {
313
+ callbacks.onSuccess?.(state.longPollResult, currentResult);
314
+ }
315
+ if (state.longPollFailure !== previousState.longPollFailure && state.longPollFailure) {
316
+ if (state.longPollFailure.status === 'LOCKED') {
317
+ callbacks.onLocked?.(state.longPollFailure, currentResult);
318
+ }
319
+ if (state.longPollFailure.status === 'EXPIRED') {
320
+ callbacks.onExpired?.(state.longPollFailure, currentResult);
321
+ }
322
+ }
323
+ if (state.error !== previousState.error && state.error) {
324
+ callbacks.onError?.(state.error, currentResult);
325
+ }
326
+ if (state.status !== previousState.status) {
327
+ callbacks.onStatusChange?.(state.status, currentResult);
328
+ }
329
+ previousStateRef.current = state;
330
+ }, [state]);
331
+ return result;
332
+ }
333
+ function createInitialState() {
334
+ return {
335
+ status: 'idle',
336
+ session: null,
337
+ events: [],
338
+ lastEvent: null,
339
+ subscribedEvent: null,
340
+ subscribedExpiresAt: null,
341
+ lastAttemptEvent: null,
342
+ lockedEvent: null,
343
+ verifiedEvent: null,
344
+ expiredEvent: null,
345
+ longPollResult: null,
346
+ longPollFailure: null,
347
+ attempts: 0,
348
+ triesLeft: null,
349
+ error: null,
350
+ isWaiting: false,
351
+ };
352
+ }
353
+ function createResultInput(state, waitMode, lastRequest) {
354
+ const sessionId = resolveSessionId(state);
355
+ const eventLog = createEventLog(state);
356
+ const latestEventText = eventLog.at(-1)?.description ?? null;
357
+ const message = createMessage(state, sessionId, latestEventText);
358
+ const called = state.status !== 'idle' ||
359
+ sessionId !== null ||
360
+ state.events.length > 0 ||
361
+ state.error !== null ||
362
+ state.longPollResult !== null ||
363
+ state.longPollFailure !== null;
364
+ return {
365
+ loading: state.status === 'starting' || state.isWaiting,
366
+ called,
367
+ hasSession: state.session !== null,
368
+ message,
369
+ latestEventText,
370
+ eventLog,
371
+ lastRequest,
372
+ waitMode,
373
+ status: state.status,
374
+ sessionId,
375
+ session: state.session,
376
+ events: state.events,
377
+ lastEvent: state.lastEvent,
378
+ subscribedEvent: state.subscribedEvent,
379
+ subscribedExpiresAt: state.subscribedExpiresAt,
380
+ lastAttemptEvent: state.lastAttemptEvent,
381
+ lockedEvent: state.lockedEvent,
382
+ verifiedEvent: state.verifiedEvent,
383
+ expiredEvent: state.expiredEvent,
384
+ longPollResult: state.longPollResult,
385
+ longPollFailure: state.longPollFailure,
386
+ attempts: state.attempts,
387
+ triesLeft: state.triesLeft,
388
+ error: state.error,
389
+ isIdle: state.status === 'idle',
390
+ isStarting: state.status === 'starting',
391
+ isWaiting: state.isWaiting,
392
+ isVerified: state.status === 'verified',
393
+ isLocked: state.status === 'locked',
394
+ isExpired: state.status === 'expired',
395
+ };
396
+ }
397
+ function createHookResult(input, data, actions) {
398
+ return {
399
+ data,
400
+ loading: input.loading,
401
+ called: input.called,
402
+ hasSession: input.hasSession,
403
+ message: input.message,
404
+ latestEventText: input.latestEventText,
405
+ eventLog: input.eventLog,
406
+ lastRequest: input.lastRequest,
407
+ waitMode: input.waitMode,
408
+ status: input.status,
409
+ sessionId: input.sessionId,
410
+ session: input.session,
411
+ events: input.events,
412
+ lastEvent: input.lastEvent,
413
+ subscribedEvent: input.subscribedEvent,
414
+ subscribedExpiresAt: input.subscribedExpiresAt,
415
+ lastAttemptEvent: input.lastAttemptEvent,
416
+ lockedEvent: input.lockedEvent,
417
+ verifiedEvent: input.verifiedEvent,
418
+ expiredEvent: input.expiredEvent,
419
+ longPollResult: input.longPollResult,
420
+ longPollFailure: input.longPollFailure,
421
+ attempts: input.attempts,
422
+ triesLeft: input.triesLeft,
423
+ error: input.error,
424
+ isIdle: input.isIdle,
425
+ isStarting: input.isStarting,
426
+ isWaiting: input.isWaiting,
427
+ isVerified: input.isVerified,
428
+ isLocked: input.isLocked,
429
+ isExpired: input.isExpired,
430
+ create: actions.start,
431
+ restart: actions.restart,
432
+ wait: actions.waitForSession,
433
+ stop: actions.cancel,
434
+ clear: actions.reset,
435
+ start: actions.start,
436
+ waitForSession: actions.waitForSession,
437
+ cancel: actions.cancel,
438
+ reset: actions.reset,
439
+ };
440
+ }
441
+ function createMessage(state, sessionId, latestEventText) {
442
+ if (state.error) {
443
+ return state.error.message;
444
+ }
445
+ if (latestEventText) {
446
+ return latestEventText;
447
+ }
448
+ if (state.isWaiting && sessionId) {
449
+ return `Waiting for session ${sessionId} to finish.`;
450
+ }
451
+ if (state.session && sessionId) {
452
+ return `Session ${sessionId} is ready.`;
453
+ }
454
+ return null;
455
+ }
456
+ function createEventLog(state) {
457
+ const log = state.events.map((event) => createEventLogItem(event));
458
+ if (state.longPollResult) {
459
+ log.push({
460
+ type: 'otp.verified',
461
+ description: describeLongPollSuccess(state.longPollResult),
462
+ occurredAt: state.longPollResult.verifiedAt,
463
+ payload: state.longPollResult,
464
+ });
465
+ }
466
+ if (state.longPollFailure?.status === 'LOCKED') {
467
+ log.push({
468
+ type: 'otp.locked',
469
+ description: describeLongPollFailure(state.longPollFailure),
470
+ occurredAt: state.longPollFailure.lockedAt ?? null,
471
+ payload: state.longPollFailure,
472
+ });
473
+ }
474
+ if (state.longPollFailure?.status === 'EXPIRED') {
475
+ log.push({
476
+ type: 'otp.expired',
477
+ description: describeLongPollFailure(state.longPollFailure),
478
+ occurredAt: state.longPollFailure.expiredAt ?? null,
479
+ payload: state.longPollFailure,
480
+ });
481
+ }
482
+ return log;
483
+ }
484
+ function createEventLogItem(event) {
485
+ return {
486
+ type: event.event,
487
+ description: describeSessionEvent(event),
488
+ occurredAt: getSessionEventOccurredAt(event),
489
+ payload: event,
490
+ };
491
+ }
492
+ function describeSessionEvent(event) {
493
+ if (event.event === 'otp.subscribed') {
494
+ return `session ${event.data.sessionId} subscribed`;
495
+ }
496
+ if (event.event === 'otp.attempt') {
497
+ return `attempt ${event.data.attempts} of ${event.data.maxTries}, ${event.data.triesLeft} left`;
498
+ }
499
+ if (event.event === 'otp.locked') {
500
+ return `locked after ${event.data.attempts} attempt(s)`;
501
+ }
502
+ if (event.event === 'otp.verified') {
503
+ return `verified at ${event.data.verifiedAt}`;
504
+ }
505
+ return `expired session ${event.data.sessionId}`;
506
+ }
507
+ function describeLongPollSuccess(result) {
508
+ return `verified at ${result.verifiedAt}`;
509
+ }
510
+ function describeLongPollFailure(result) {
511
+ if (result.status === 'LOCKED') {
512
+ const attempts = typeof result.attempts === 'number' ? result.attempts : '?';
513
+ return `locked after ${attempts} attempt(s)`;
514
+ }
515
+ return `expired session ${result.sessionId}`;
516
+ }
517
+ function getSessionEventOccurredAt(event) {
518
+ if (event.event === 'otp.subscribed') {
519
+ return event.data.expiresAt;
520
+ }
521
+ if (event.event === 'otp.attempt') {
522
+ return event.data.attemptedAt;
523
+ }
524
+ if (event.event === 'otp.locked') {
525
+ return event.data.lockedAt;
526
+ }
527
+ if (event.event === 'otp.verified') {
528
+ return event.data.verifiedAt;
529
+ }
530
+ return null;
531
+ }
532
+ function resolveSessionId(state) {
533
+ return (state.session?.sessionId ??
534
+ state.verifiedEvent?.data.sessionId ??
535
+ state.lockedEvent?.data.sessionId ??
536
+ state.expiredEvent?.data.sessionId ??
537
+ state.longPollResult?.sessionId ??
538
+ state.longPollFailure?.sessionId ??
539
+ null);
540
+ }
541
+ function recordEvent(current, event, updates = {}) {
542
+ return {
543
+ ...current,
544
+ ...updates,
545
+ events: [...current.events, event],
546
+ lastEvent: event,
547
+ error: updates.error ?? current.error,
548
+ };
549
+ }
550
+ function resolveClient(options) {
551
+ if (options.client) {
552
+ return options.client;
553
+ }
554
+ return new SiymoOtpClient({
555
+ ...options.clientOptions,
556
+ baseUrl: options.baseUrl ?? resolveDefaultSiymoOtpBaseUrl(),
557
+ websocketBaseUrl: options.websocketBaseUrl,
558
+ });
559
+ }
560
+ function normalizeError(error) {
561
+ if (error instanceof Error) {
562
+ return error;
563
+ }
564
+ return new Error(typeof error === 'string' ? error : 'Unknown OTP hook error');
565
+ }
566
+ function isAbortLikeError(error) {
567
+ if (error instanceof Error && error.name === 'AbortError') {
568
+ return true;
569
+ }
570
+ if (error instanceof SiymoOtpApiError && error.status === 0) {
571
+ const cause = getErrorCause(error);
572
+ return cause instanceof Error && cause.name === 'AbortError';
573
+ }
574
+ return false;
575
+ }
576
+ function getErrorCause(error) {
577
+ const value = error;
578
+ return value.cause;
579
+ }
580
+ function extractLongPollFailure(error) {
581
+ if (!(error instanceof SiymoOtpApiError)) {
582
+ return null;
583
+ }
584
+ if (isLongPollFailureResponse(error.body)) {
585
+ return error.body;
586
+ }
587
+ return null;
588
+ }
589
+ function isLongPollFailureResponse(value) {
590
+ if (!isRecord(value)) {
591
+ return false;
592
+ }
593
+ return (typeof value.sessionId === 'string' &&
594
+ typeof value.message === 'string' &&
595
+ (value.status === 'LOCKED' || value.status === 'EXPIRED'));
596
+ }
597
+ function isRecord(value) {
598
+ return typeof value === 'object' && value !== null;
599
+ }
600
+ function isCurrentActiveWait(activeWaitRef, activeWait) {
601
+ return activeWaitRef.current?.id === activeWait.id;
602
+ }
603
+ async function stopActiveWait(activeWaitRef) {
604
+ const activeWait = activeWaitRef.current;
605
+ if (!activeWait) {
606
+ return;
607
+ }
608
+ finalizeActiveWait(activeWaitRef, activeWait);
609
+ activeWait.controller.abort();
610
+ activeWait.subscription?.close(1000, 'Cancelled');
611
+ if (activeWait.subscription) {
612
+ try {
613
+ await activeWait.subscription.closed;
614
+ }
615
+ catch {
616
+ // No extra handling needed during local hook cleanup.
617
+ }
618
+ }
619
+ }
620
+ function finalizeActiveWait(activeWaitRef, activeWait) {
621
+ if (activeWait.timeoutId) {
622
+ clearTimeout(activeWait.timeoutId);
623
+ }
624
+ if (activeWaitRef.current?.id === activeWait.id) {
625
+ activeWaitRef.current = null;
626
+ }
627
+ }
628
+ function scheduleWaitTimeout(activeWaitRef, activeWait, timeoutMs, sessionId, updateState) {
629
+ if (typeof timeoutMs !== 'number' || timeoutMs <= 0) {
630
+ return;
631
+ }
632
+ activeWait.timeoutId = setTimeout(() => {
633
+ if (!isCurrentActiveWait(activeWaitRef, activeWait)) {
634
+ return;
635
+ }
636
+ finalizeActiveWait(activeWaitRef, activeWait);
637
+ activeWait.controller.abort();
638
+ activeWait.subscription?.close(1000, 'Timed out');
639
+ updateState((current) => ({
640
+ ...current,
641
+ status: 'error',
642
+ error: new Error(`Timed out after ${timeoutMs} ms while waiting for session ${sessionId}.`),
643
+ isWaiting: false,
644
+ }));
645
+ }, timeoutMs);
646
+ }
@@ -0,0 +1,2 @@
1
+ import type { UseInboundSmsOtpOptions, UseInboundSmsOtpResult } from './types.js';
2
+ export declare function useInboundSmsOtp(options?: UseInboundSmsOtpOptions): UseInboundSmsOtpResult;
@@ -0,0 +1,65 @@
1
+ import { useInboundOtpSession } from './useInboundOtp.js';
2
+ export function useInboundSmsOtp(options = {}) {
3
+ const result = useInboundOtpSession({
4
+ options,
5
+ initiate(client, request) {
6
+ return client.initiateInboundSms(request);
7
+ },
8
+ selectData(input) {
9
+ return {
10
+ session: input.session,
11
+ sessionId: input.sessionId,
12
+ channel: input.session?.channel ?? null,
13
+ direction: input.session?.direction ?? null,
14
+ status: input.status,
15
+ attempts: input.attempts,
16
+ triesLeft: input.triesLeft,
17
+ length: input.session?.length ?? null,
18
+ expiresIn: input.session?.expiresIn ?? null,
19
+ maxTries: input.session?.maxTries ??
20
+ input.lockedEvent?.data.maxTries ??
21
+ input.longPollFailure?.maxTries ??
22
+ null,
23
+ subscribedExpiresAt: input.subscribedExpiresAt,
24
+ lastEvent: input.lastEvent,
25
+ lastEventText: input.latestEventText,
26
+ servicePhone: input.session?.phone ??
27
+ input.verifiedEvent?.data.serverPhone ??
28
+ input.lockedEvent?.data.serverPhone ??
29
+ input.longPollResult?.serverPhone ??
30
+ input.longPollFailure?.serverPhone ??
31
+ null,
32
+ senderPhone: input.session?.from ??
33
+ input.verifiedEvent?.data.phone ??
34
+ input.lockedEvent?.data.phone ??
35
+ input.longPollResult?.phone ??
36
+ input.longPollFailure?.phone ??
37
+ null,
38
+ qrCodeImage: input.session?.qrCodeImage ?? null,
39
+ rawText: input.session?.text ?? null,
40
+ decodedText: decodeBase64Text(input.session?.text),
41
+ };
42
+ },
43
+ });
44
+ return {
45
+ ...result,
46
+ decodedText: result.data.decodedText,
47
+ qrCodeImage: result.data.qrCodeImage,
48
+ };
49
+ }
50
+ function decodeBase64Text(value) {
51
+ if (!value) {
52
+ return null;
53
+ }
54
+ if (typeof globalThis.atob !== 'function') {
55
+ return null;
56
+ }
57
+ try {
58
+ const binary = globalThis.atob(value);
59
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
60
+ return new TextDecoder().decode(bytes);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@siymo/otp-sdk-react",
3
+ "version": "0.1.0",
4
+ "description": "React hooks SDK for inbound call and inbound SMS OTP flows built on top of @siymo/otp-sdk-core",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "sideEffects": false,
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "scripts": {
23
+ "prebuild": "node ./scripts/write-build-config.mjs",
24
+ "build": "tsc -p tsconfig.json",
25
+ "prepare": "npm run build",
26
+ "prepack": "npm run build"
27
+ },
28
+ "keywords": [
29
+ "otp",
30
+ "react",
31
+ "hooks",
32
+ "sdk",
33
+ "websocket"
34
+ ],
35
+ "peerDependencies": {
36
+ "react": ">=18",
37
+ "@siymo/otp-sdk-core": "^1.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@siymo/otp-sdk-core": "^1.0.0",
41
+ "@types/react": "^18.3.3",
42
+ "react": "^18.3.1",
43
+ "typescript": "^5.0.0"
44
+ }
45
+ }