react-native-otp-auto-verify 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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kailas Rathod
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,20 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "OtpAutoVerify"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/kailas-rathod/react-native-otp-auto-verify.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ install_modules_dependencies(s)
20
+ end
package/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # react-native-otp-auto-verify
2
+
3
+ <div align="center">
4
+
5
+ [![npm version](https://img.shields.io/npm/v/react-native-otp-auto-verify.svg?style=flat-square)](https://www.npmjs.com/package/react-native-otp-auto-verify)
6
+ [![npm downloads](https://img.shields.io/npm/dm/react-native-otp-auto-verify.svg?style=flat-square)](https://www.npmjs.com/package/react-native-otp-auto-verify)
7
+ [![license](https://img.shields.io/npm/l/react-native-otp-auto-verify.svg?style=flat-square)](https://github.com/kailas-rathod/react-native-otp-auto-verify/blob/main/LICENSE)
8
+ [![typescript](https://img.shields.io/badge/TypeScript-Ready-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
9
+
10
+ Automatic OTP detection on **Android** using the **Google SMS Retriever API** (no `READ_SMS` permission required).
11
+
12
+ </div>
13
+
14
+ ## Table of contents
15
+
16
+ - [Features](#features)
17
+ - [Platform support](#platform-support)
18
+ - [Requirements](#requirements)
19
+ - [Installation](#installation)
20
+ - [Usage](#usage)
21
+ - [1) Get your app hash](#1-get-your-app-hash)
22
+ - [2) Format your OTP SMS](#2-format-your-otp-sms)
23
+ - [3) Hook usage (recommended)](#3-hook-usage-recommended)
24
+ - [4) Imperative usage](#4-imperative-usage)
25
+ - [API](#api)
26
+ - [Timeout behavior](#timeout-behavior)
27
+ - [React Native New Architecture](#react-native-new-architecture)
28
+ - [Troubleshooting](#troubleshooting)
29
+ - [Example app](#example-app)
30
+ - [Contributing](#contributing)
31
+ - [License](#license)
32
+
33
+ ## Features
34
+
35
+ - Automatically extracts **4–6 digit** OTPs from SMS on Android
36
+ - Uses Google **SMS Retriever API** (Play Store compliant, **no SMS permissions**)
37
+ - Includes a **React hook** and an **imperative** API
38
+ - Fully typed (**TypeScript**)
39
+ - **React Native New Architecture** (TurboModules) supported
40
+ - iOS is a **safe no-op** (manual OTP entry required)
41
+
42
+ ## Platform support
43
+
44
+ | Platform | Support | Notes |
45
+ | -------- | ------- | --------------------------------------- |
46
+ | Android | ✅ | Requires Google Play services on device |
47
+ | iOS | ⚠️ | Safe no-op (no auto-read) |
48
+
49
+ ## Requirements
50
+
51
+ - React Native: **0.60+** (autolinking)
52
+ - Android: **minSdkVersion 24+**
53
+ - iOS: supported as a **no-op** (you still need manual OTP UI)
54
+
55
+ ## Installation
56
+
57
+ ```sh
58
+ npm install react-native-otp-auto-verify
59
+ # or
60
+ yarn add react-native-otp-auto-verify
61
+ # or
62
+ pnpm add react-native-otp-auto-verify
63
+ ```
64
+
65
+ iOS (React Native CLI projects):
66
+
67
+ ```sh
68
+ npx pod-install
69
+ ```
70
+
71
+ ## Usage
72
+
73
+ ### 1) Get your app hash
74
+
75
+ SMS Retriever only delivers messages that include your **11-character app hash**.
76
+
77
+ ```ts
78
+ import { getHash } from 'react-native-otp-auto-verify';
79
+
80
+ const hashes = await getHash();
81
+ const appHash = hashes[0]; // send this to your backend
82
+ ```
83
+
84
+ ### 2) Format your OTP SMS
85
+
86
+ Your backend **must** include the app hash at the **end** of the SMS.
87
+
88
+ Requirements:
89
+
90
+ - Message must be **≤ 140 bytes**
91
+ - Must contain a **4–6 digit** OTP
92
+ - Must **end with** the app hash from `getHash()`
93
+
94
+ Recommended format:
95
+
96
+ ```
97
+ <#> Your verification code is 123456
98
+ AbCdEfGhIjK
99
+ ```
100
+
101
+ ### 3) Hook usage (recommended)
102
+
103
+ Start listening only while the OTP screen is visible (foreground).
104
+
105
+ ```tsx
106
+ import React from 'react';
107
+ import { Text, View } from 'react-native';
108
+ import { useOtpVerification } from 'react-native-otp-auto-verify';
109
+
110
+ export function OtpScreen() {
111
+ const { hashCode, otp, timeoutError, startListening, stopListening } =
112
+ useOtpVerification({ numberOfDigits: 6 });
113
+
114
+ React.useEffect(() => {
115
+ void startListening();
116
+ return () => stopListening();
117
+ }, [startListening, stopListening]);
118
+
119
+ return (
120
+ <View>
121
+ {!!hashCode && <Text>Hash: {hashCode}</Text>}
122
+ {!!otp && <Text>OTP: {otp}</Text>}
123
+ {timeoutError && <Text>Timeout. Tap resend and try again.</Text>}
124
+ </View>
125
+ );
126
+ }
127
+ ```
128
+
129
+ ### 4) Imperative usage
130
+
131
+ ```ts
132
+ import {
133
+ activateOtpListener,
134
+ removeListener,
135
+ } from 'react-native-otp-auto-verify';
136
+
137
+ const sub = await activateOtpListener(
138
+ (sms, extractedOtp) => {
139
+ if (extractedOtp) {
140
+ console.log('OTP:', extractedOtp);
141
+ }
142
+ },
143
+ { numberOfDigits: 6 }
144
+ );
145
+
146
+ // cleanup
147
+ sub.remove();
148
+ // or
149
+ removeListener();
150
+ ```
151
+
152
+ ## API
153
+
154
+ ### `useOtpVerification(options?)`
155
+
156
+ - **options**
157
+ - `numberOfDigits?: 4 | 5 | 6` (default `6`)
158
+ - **returns**
159
+ - `hashCode: string`
160
+ - `otp: string | null`
161
+ - `sms: string | null`
162
+ - `timeoutError: boolean`
163
+ - `startListening(): Promise<void>`
164
+ - `stopListening(): void`
165
+
166
+ ### `getHash(): Promise<string[]>`
167
+
168
+ Android only. On iOS returns `[]`.
169
+
170
+ ### `activateOtpListener(handler, options?): Promise<{ remove(): void }>`
171
+
172
+ Android only. Throws on iOS.
173
+
174
+ ### `removeListener(): void`
175
+
176
+ Android only. Removes native listeners.
177
+
178
+ ### `extractOtp(sms: string, numberOfDigits?: 4 | 5 | 6): string | null`
179
+
180
+ Pure helper to extract the OTP from an SMS string.
181
+
182
+ ## Timeout behavior
183
+
184
+ SMS Retriever waits up to **5 minutes**. When it times out:
185
+
186
+ - `timeoutError` becomes `true`
187
+ - call `startListening()` again to retry
188
+
189
+ ## React Native New Architecture
190
+
191
+ This library is built with **TurboModules** and fully supports React Native's **New Architecture**.
192
+
193
+ ### What is the New Architecture?
194
+
195
+ The New Architecture (also known as Fabric + TurboModules) is React Native's new rendering system and native module architecture that provides:
196
+
197
+ - Better performance and type safety
198
+ - Synchronous native module calls
199
+ - Improved interoperability with native code
200
+
201
+ ### Enabling New Architecture
202
+
203
+ The library works automatically with both **Old Architecture** and **New Architecture**. No code changes needed.
204
+
205
+ **For Android:**
206
+
207
+ Enable New Architecture in `android/gradle.properties`:
208
+
209
+ ```properties
210
+ newArchEnabled=true
211
+ ```
212
+
213
+ **For iOS:**
214
+
215
+ Enable New Architecture in `ios/Podfile`:
216
+
217
+ ```ruby
218
+ use_react_native!(
219
+ :fabric_enabled => true,
220
+ # ... other options
221
+ )
222
+ ```
223
+
224
+ Or set the environment variable:
225
+
226
+ ```sh
227
+ RCT_NEW_ARCH_ENABLED=1 pod install
228
+ ```
229
+
230
+ ### Compatibility
231
+
232
+ - ✅ Works with **Old Architecture** (React Native < 0.68)
233
+ - ✅ Works with **New Architecture** (React Native 0.68+)
234
+ - ✅ Automatically detects and uses the correct architecture
235
+ - ✅ No breaking changes when migrating
236
+
237
+ ## Troubleshooting
238
+
239
+ - OTP not detected
240
+ - Ensure the SMS **ends with** the app hash
241
+ - Keep the SMS **≤ 140 bytes**
242
+ - Match `numberOfDigits` to your OTP length
243
+ - Keep the app in **foreground**
244
+ - Listener fails to start
245
+ - Ensure Google Play services are available on the device
246
+ - Avoid multiple active listeners at once
247
+
248
+ ## Example app
249
+
250
+ See [`./example`](./example).
251
+
252
+ ## Contributing
253
+
254
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md).
255
+
256
+ ## License
257
+
258
+ MIT
@@ -0,0 +1,68 @@
1
+ buildscript {
2
+ ext.OtpAutoVerify = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return OtpAutoVerify[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.7.2"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+
30
+ apply plugin: "com.android.library"
31
+ apply plugin: "kotlin-android"
32
+
33
+ apply plugin: "com.facebook.react"
34
+
35
+ android {
36
+ namespace "com.otpautoverify"
37
+
38
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
39
+
40
+ defaultConfig {
41
+ minSdkVersion getExtOrDefault("minSdkVersion")
42
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
43
+ }
44
+
45
+ buildFeatures {
46
+ buildConfig true
47
+ }
48
+
49
+ buildTypes {
50
+ release {
51
+ minifyEnabled false
52
+ }
53
+ }
54
+
55
+ lint {
56
+ disable "GradleCompatible"
57
+ }
58
+
59
+ compileOptions {
60
+ sourceCompatibility JavaVersion.VERSION_1_8
61
+ targetCompatibility JavaVersion.VERSION_1_8
62
+ }
63
+ }
64
+
65
+ dependencies {
66
+ implementation "com.facebook.react:react-android"
67
+ implementation "com.google.android.gms:play-services-auth:21.3.0"
68
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,80 @@
1
+ package com.otpautoverify
2
+
3
+ import android.content.Context
4
+ import android.content.ContextWrapper
5
+ import android.content.pm.PackageManager
6
+ import android.os.Build
7
+ import android.util.Base64
8
+ import android.util.Log
9
+ import java.nio.charset.StandardCharsets
10
+ import java.security.MessageDigest
11
+ import java.security.NoSuchAlgorithmException
12
+ import java.util.ArrayList
13
+ import java.util.Arrays
14
+
15
+ /**
16
+ * Computes the 11-character app hash required by the SMS Retriever API.
17
+ * Uses GET_SIGNING_CERTIFICATES on API 28+ and GET_SIGNATURES on older versions.
18
+ */
19
+ class AppSignatureHelper(context: Context) : ContextWrapper(context) {
20
+
21
+ companion object {
22
+ private const val TAG = "AppSignatureHelper"
23
+ private const val HASH_TYPE = "SHA-256"
24
+ private const val NUM_HASHED_BYTES = 9
25
+ private const val NUM_BASE64_CHAR = 11
26
+ }
27
+
28
+ /**
29
+ * Returns app hash strings for the current package.
30
+ * Include one of these in your SMS message (e.g. at the end) for the Retriever to match.
31
+ */
32
+ fun getAppSignatures(): ArrayList<String> {
33
+ val appCodes = ArrayList<String>()
34
+ try {
35
+ val packageName = packageName
36
+ val packageManager = packageManager
37
+ val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
38
+ val packageInfo = packageManager.getPackageInfo(
39
+ packageName,
40
+ PackageManager.GET_SIGNING_CERTIFICATES
41
+ )
42
+ packageInfo.signingInfo?.apkContentsSigners
43
+ } else {
44
+ @Suppress("DEPRECATION")
45
+ val packageInfo = packageManager.getPackageInfo(
46
+ packageName,
47
+ PackageManager.GET_SIGNATURES
48
+ )
49
+ packageInfo.signatures
50
+ } ?: return appCodes
51
+
52
+ for (signature in signatures) {
53
+ val hash = hash(packageName, signature.toCharsString())
54
+ if (hash != null) {
55
+ appCodes.add(hash)
56
+ }
57
+ }
58
+ } catch (e: PackageManager.NameNotFoundException) {
59
+ Log.e(TAG, "Unable to find package to obtain hash.", e)
60
+ }
61
+ return appCodes
62
+ }
63
+
64
+ private fun hash(packageName: String, signature: String): String? {
65
+ return try {
66
+ val appInfo = "$packageName $signature"
67
+ val messageDigest = MessageDigest.getInstance(HASH_TYPE)
68
+ messageDigest.update(appInfo.toByteArray(StandardCharsets.UTF_8))
69
+ var hashSignature = messageDigest.digest()
70
+ hashSignature = Arrays.copyOfRange(hashSignature, 0, NUM_HASHED_BYTES)
71
+ var base64Hash = Base64.encodeToString(hashSignature, Base64.NO_PADDING or Base64.NO_WRAP)
72
+ base64Hash = base64Hash.substring(0, minOf(NUM_BASE64_CHAR, base64Hash.length))
73
+ Log.d(TAG, "pkg: $packageName -- hash: $base64Hash")
74
+ base64Hash
75
+ } catch (e: NoSuchAlgorithmException) {
76
+ Log.e(TAG, "hash: NoSuchAlgorithm", e)
77
+ null
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,146 @@
1
+ package com.otpautoverify
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.IntentFilter
5
+ import android.os.Build
6
+ import android.util.Log
7
+ import com.facebook.react.bridge.Arguments
8
+ import com.facebook.react.bridge.LifecycleEventListener
9
+ import com.facebook.react.bridge.Promise
10
+ import com.facebook.react.bridge.ReactApplicationContext
11
+ import com.google.android.gms.auth.api.phone.SmsRetriever
12
+ import com.google.android.gms.tasks.OnFailureListener
13
+ import com.google.android.gms.tasks.OnSuccessListener
14
+ class OtpAutoVerifyModule(reactContext: ReactApplicationContext) :
15
+ NativeOtpAutoVerifySpec(reactContext), LifecycleEventListener {
16
+
17
+ companion object {
18
+ const val NAME = NativeOtpAutoVerifySpec.NAME
19
+ private const val TAG = "OtpAutoVerifyModule"
20
+ const val OTP_RECEIVED_EVENT = "otpReceived"
21
+ }
22
+
23
+ private var smsReceiver: SmsRetrieverBroadcastReceiver? = null
24
+ private var isReceiverRegistered = false
25
+ private var isListening = false
26
+
27
+ init {
28
+ reactContext.addLifecycleEventListener(this)
29
+ }
30
+
31
+ override fun getTypedExportedConstants(): MutableMap<String, Any> {
32
+ return mutableMapOf("OTP_RECEIVED_EVENT" to OTP_RECEIVED_EVENT)
33
+ }
34
+
35
+ override fun getHash(promise: Promise) {
36
+ try {
37
+ val helper = AppSignatureHelper(reactApplicationContext)
38
+ val signatures = helper.getAppSignatures()
39
+ val arr = Arguments.createArray()
40
+ for (s in signatures) {
41
+ arr.pushString(s)
42
+ }
43
+ promise.resolve(arr)
44
+ } catch (e: Exception) {
45
+ Log.e(TAG, "getHash failed", e)
46
+ promise.reject("GET_HASH_ERROR", e.message, e)
47
+ }
48
+ }
49
+
50
+ override fun startSmsRetriever(promise: Promise) {
51
+ val activity = currentActivity
52
+ if (activity == null) {
53
+ promise.reject("NO_ACTIVITY", "No current activity. Ensure the app is in the foreground.")
54
+ return
55
+ }
56
+
57
+ registerReceiverIfNecessary(activity)
58
+
59
+ val client = SmsRetriever.getClient(reactApplicationContext)
60
+ client.startSmsRetriever()
61
+ .addOnSuccessListener(OnSuccessListener {
62
+ Log.d(TAG, "SMS retriever started")
63
+ isListening = true
64
+ promise.resolve(true)
65
+ })
66
+ .addOnFailureListener(OnFailureListener { e ->
67
+ Log.e(TAG, "Failed to start SMS retriever", e)
68
+ promise.reject("START_SMS_RETRIEVER_ERROR", e.message, e)
69
+ })
70
+ }
71
+
72
+ @SuppressLint("UnspecifiedRegisterReceiverFlag")
73
+ private fun registerReceiverIfNecessary(activity: android.app.Activity) {
74
+ if (isReceiverRegistered) return
75
+ try {
76
+ smsReceiver = SmsRetrieverBroadcastReceiver(reactApplicationContext, OTP_RECEIVED_EVENT)
77
+ val filter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
78
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
79
+ activity.registerReceiver(
80
+ smsReceiver,
81
+ filter,
82
+ SmsRetriever.SEND_PERMISSION,
83
+ null,
84
+ android.content.Context.RECEIVER_EXPORTED
85
+ )
86
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
87
+ activity.registerReceiver(
88
+ smsReceiver,
89
+ filter,
90
+ android.content.Context.RECEIVER_EXPORTED
91
+ )
92
+ } else {
93
+ activity.registerReceiver(smsReceiver, filter)
94
+ }
95
+ isReceiverRegistered = true
96
+ Log.d(TAG, "SMS receiver registered")
97
+ } catch (e: Exception) {
98
+ Log.e(TAG, "Failed to register SMS receiver", e)
99
+ }
100
+ }
101
+
102
+ private fun unregisterReceiver() {
103
+ val activity = currentActivity
104
+ if (isReceiverRegistered && activity != null && smsReceiver != null) {
105
+ try {
106
+ activity.unregisterReceiver(smsReceiver)
107
+ Log.d(TAG, "SMS receiver unregistered")
108
+ } catch (e: Exception) {
109
+ Log.e(TAG, "Failed to unregister SMS receiver", e)
110
+ }
111
+ isReceiverRegistered = false
112
+ smsReceiver = null
113
+ }
114
+ isListening = false
115
+ }
116
+
117
+ override fun addListener(eventName: String) {
118
+ // Required for NativeEventEmitter; no-op.
119
+ }
120
+
121
+ override fun removeListeners(count: Double) {
122
+ unregisterReceiver()
123
+ }
124
+
125
+ override fun onHostResume() {
126
+ // Optionally re-register if we were listening and activity was recreated.
127
+ if (isListening && currentActivity != null && !isReceiverRegistered) {
128
+ currentActivity?.let { registerReceiverIfNecessary(it) }
129
+ }
130
+ }
131
+
132
+ override fun onHostPause() {
133
+ unregisterReceiver()
134
+ }
135
+
136
+ override fun onHostDestroy() {
137
+ unregisterReceiver()
138
+ reactApplicationContext.removeLifecycleEventListener(this)
139
+ }
140
+
141
+ override fun invalidate() {
142
+ unregisterReceiver()
143
+ reactApplicationContext.removeLifecycleEventListener(this)
144
+ super.invalidate()
145
+ }
146
+ }
@@ -0,0 +1,33 @@
1
+ package com.otpautoverify
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class OtpAutoVerifyPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == OtpAutoVerifyModule.NAME) {
13
+ OtpAutoVerifyModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
20
+ return ReactModuleInfoProvider {
21
+ val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
22
+ moduleInfos[OtpAutoVerifyModule.NAME] = ReactModuleInfo(
23
+ OtpAutoVerifyModule.NAME,
24
+ OtpAutoVerifyModule.NAME,
25
+ false, // canOverrideExistingModule
26
+ false, // needsEagerInit
27
+ false, // isCxxModule
28
+ true // isTurboModule
29
+ )
30
+ moduleInfos
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,61 @@
1
+ package com.otpautoverify
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.os.Bundle
7
+ import android.util.Log
8
+ import com.facebook.react.bridge.ReactApplicationContext
9
+ import com.google.android.gms.auth.api.phone.SmsRetriever
10
+ import com.google.android.gms.common.api.CommonStatusCodes
11
+ import com.google.android.gms.common.api.Status
12
+
13
+ /**
14
+ * Receives SMS Retriever API results and forwards the message or timeout to JS via RCTDeviceEventEmitter.
15
+ * No READ_SMS permission required; fully Play Store compliant.
16
+ */
17
+ class SmsRetrieverBroadcastReceiver(
18
+ private val reactContext: ReactApplicationContext,
19
+ private val eventName: String
20
+ ) : BroadcastReceiver() {
21
+
22
+ companion object {
23
+ private const val TAG = "SmsRetrieverReceiver"
24
+ const val TIMEOUT_MESSAGE = "Timeout Error."
25
+ }
26
+
27
+ override fun onReceive(context: Context?, intent: Intent?) {
28
+ if (intent?.action != SmsRetriever.SMS_RETRIEVED_ACTION) return
29
+ val extras: Bundle = intent.extras ?: return
30
+ val status = extras.get(SmsRetriever.EXTRA_STATUS) as? Status ?: return
31
+
32
+ when (status.statusCode) {
33
+ CommonStatusCodes.SUCCESS -> {
34
+ val message = extras.getString(SmsRetriever.EXTRA_SMS_MESSAGE)
35
+ if (message != null) {
36
+ Log.d(TAG, "SMS received")
37
+ emitMessage(message)
38
+ }
39
+ }
40
+ CommonStatusCodes.TIMEOUT -> {
41
+ Log.d(TAG, "SMS retriever timeout")
42
+ emitMessage(TIMEOUT_MESSAGE)
43
+ }
44
+ else -> {
45
+ Log.w(TAG, "SMS retriever status: ${status.statusCode}")
46
+ }
47
+ }
48
+ }
49
+
50
+ private fun emitMessage(message: String) {
51
+ if (!reactContext.hasActiveReactInstance()) return
52
+ reactContext.runOnJSQueueThread {
53
+ try {
54
+ reactContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
55
+ ?.emit(eventName, message)
56
+ } catch (e: Exception) {
57
+ Log.e(TAG, "Failed to emit OTP event", e)
58
+ }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,5 @@
1
+ #import <OtpAutoVerifySpec/OtpAutoVerifySpec.h>
2
+
3
+ @interface OtpAutoVerify : NSObject <NativeOtpAutoVerifySpec>
4
+
5
+ @end
@@ -0,0 +1,36 @@
1
+ #import "OtpAutoVerify.h"
2
+
3
+ @implementation OtpAutoVerify
4
+
5
+ - (NSDictionary *)getConstants {
6
+ return @{ @"OTP_RECEIVED_EVENT": @"otpReceived" };
7
+ }
8
+
9
+ - (void)getHash:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
10
+ resolve(@[]);
11
+ }
12
+
13
+ - (void)startSmsRetriever:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
14
+ resolve(@NO);
15
+ }
16
+
17
+ - (void)addListener:(NSString *)eventName {
18
+ // No-op on iOS; SMS Retriever is Android-only.
19
+ }
20
+
21
+ - (void)removeListeners:(double)count {
22
+ // No-op on iOS.
23
+ }
24
+
25
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
26
+ (const facebook::react::ObjCTurboModule::InitParams &)params
27
+ {
28
+ return std::make_shared<facebook::react::NativeOtpAutoVerifySpecJSI>(params);
29
+ }
30
+
31
+ + (NSString *)moduleName
32
+ {
33
+ return @"OtpAutoVerify";
34
+ }
35
+
36
+ @end
@@ -0,0 +1,11 @@
1
+ import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
+
3
+ export interface Spec extends TurboModule {
4
+ getConstants(): { OTP_RECEIVED_EVENT: string };
5
+ getHash(): Promise<ReadonlyArray<string>>;
6
+ startSmsRetriever(): Promise<boolean>;
7
+ addListener(eventName: string): void;
8
+ removeListeners(count: number): void;
9
+ }
10
+
11
+ export default TurboModuleRegistry.getEnforcing<Spec>('OtpAutoVerify');
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+
3
+ import * as React from 'react';
4
+ import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
5
+ import NativeOtpAutoVerify from './NativeOtpAutoVerify';
6
+ const {
7
+ OtpAutoVerify
8
+ } = NativeModules;
9
+ const eventEmitter = Platform.OS === 'android' && OtpAutoVerify ? new NativeEventEmitter(OtpAutoVerify) : null;
10
+ const OTP_RECEIVED_EVENT = NativeOtpAutoVerify.getConstants?.()?.OTP_RECEIVED_EVENT ?? 'otpReceived';
11
+ const TIMEOUT_MESSAGE = 'Timeout Error.';
12
+ const DEFAULT_DIGITS = 6;
13
+ const OTP_REGEX = {
14
+ 4: /\b(\d{4})\b/,
15
+ 5: /\b(\d{5})\b/,
16
+ 6: /\b(\d{6})\b/
17
+ };
18
+ /** Extracts a numeric OTP of 4–6 digits from SMS text. */
19
+ export function extractOtp(sms, numberOfDigits = DEFAULT_DIGITS) {
20
+ if (!sms || typeof sms !== 'string') return null;
21
+ const match = sms.match(OTP_REGEX[numberOfDigits]);
22
+ return match ? match[1] : null;
23
+ }
24
+
25
+ /** Returns app hash strings for the current app. Android only; iOS returns []. */
26
+ export async function getHash() {
27
+ if (Platform.OS !== 'android') return [];
28
+ const arr = await NativeOtpAutoVerify.getHash();
29
+ return Array.from(arr);
30
+ }
31
+
32
+ /** Starts SMS Retriever and subscribes to OTP events. Returns subscription with remove(). */
33
+ export async function activateOtpListener(handler, options) {
34
+ if (Platform.OS !== 'android' || !eventEmitter) {
35
+ throw new Error('SMS Retriever is only supported on Android.');
36
+ }
37
+ const numberOfDigits = options?.numberOfDigits ?? DEFAULT_DIGITS;
38
+ const subscription = eventEmitter.addListener(OTP_RECEIVED_EVENT, (...args) => {
39
+ const smsText = String(args[0] ?? '');
40
+ handler(smsText, extractOtp(smsText, numberOfDigits));
41
+ });
42
+ await NativeOtpAutoVerify.startSmsRetriever();
43
+ return {
44
+ remove: () => {
45
+ subscription.remove();
46
+ NativeOtpAutoVerify.removeListeners(0);
47
+ }
48
+ };
49
+ }
50
+
51
+ /** Stops SMS listening and removes all listeners. */
52
+ export function removeListener() {
53
+ if (Platform.OS === 'android') {
54
+ NativeOtpAutoVerify.removeListeners(0);
55
+ }
56
+ }
57
+
58
+ /** Hook for OTP verification. Stops listener on unmount. */
59
+ export function useOtpVerification(options = {}) {
60
+ const numberOfDigits = options.numberOfDigits ?? DEFAULT_DIGITS;
61
+ const [hashCode, setHashCode] = React.useState('');
62
+ const [otp, setOtp] = React.useState(null);
63
+ const [sms, setSms] = React.useState(null);
64
+ const [timeoutError, setTimeoutError] = React.useState(false);
65
+ const subscriptionRef = React.useRef(null);
66
+ const stopListening = React.useCallback(() => {
67
+ subscriptionRef.current?.remove();
68
+ subscriptionRef.current = null;
69
+ removeListener();
70
+ }, []);
71
+ const startListening = React.useCallback(async () => {
72
+ if (Platform.OS !== 'android') return;
73
+ setOtp(null);
74
+ setSms(null);
75
+ setTimeoutError(false);
76
+ try {
77
+ const hashes = await getHash();
78
+ setHashCode(hashes[0] ?? '');
79
+ } catch {
80
+ // getHash failed; continue to start listener
81
+ }
82
+ try {
83
+ const sub = await activateOtpListener((smsText, extractedOtp) => {
84
+ setSms(smsText);
85
+ if (smsText === TIMEOUT_MESSAGE) {
86
+ setTimeoutError(true);
87
+ return;
88
+ }
89
+ if (extractedOtp) setOtp(extractedOtp);
90
+ }, {
91
+ numberOfDigits
92
+ });
93
+ subscriptionRef.current = sub;
94
+ } catch {
95
+ subscriptionRef.current = null;
96
+ throw new Error('Failed to start OTP listener');
97
+ }
98
+ }, [numberOfDigits]);
99
+ React.useEffect(() => () => stopListening(), [stopListening]);
100
+ return React.useMemo(() => ({
101
+ hashCode,
102
+ otp,
103
+ sms,
104
+ timeoutError,
105
+ startListening,
106
+ stopListening
107
+ }), [hashCode, otp, sms, timeoutError, startListening, stopListening]);
108
+ }
109
+ export { OTP_RECEIVED_EVENT };
110
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["React","NativeEventEmitter","NativeModules","Platform","NativeOtpAutoVerify","OtpAutoVerify","eventEmitter","OS","OTP_RECEIVED_EVENT","getConstants","TIMEOUT_MESSAGE","DEFAULT_DIGITS","OTP_REGEX","extractOtp","sms","numberOfDigits","match","getHash","arr","Array","from","activateOtpListener","handler","options","Error","subscription","addListener","args","smsText","String","startSmsRetriever","remove","removeListeners","removeListener","useOtpVerification","hashCode","setHashCode","useState","otp","setOtp","setSms","timeoutError","setTimeoutError","subscriptionRef","useRef","stopListening","useCallback","current","startListening","hashes","sub","extractedOtp","useEffect","useMemo"],"sourceRoot":"..\\..\\src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,13 @@
1
+ import { type TurboModule } from 'react-native';
2
+ export interface Spec extends TurboModule {
3
+ getConstants(): {
4
+ OTP_RECEIVED_EVENT: string;
5
+ };
6
+ getHash(): Promise<ReadonlyArray<string>>;
7
+ startSmsRetriever(): Promise<boolean>;
8
+ addListener(eventName: string): void;
9
+ removeListeners(count: number): void;
10
+ }
11
+ declare const _default: Spec;
12
+ export default _default;
13
+ //# sourceMappingURL=NativeOtpAutoVerify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeOtpAutoVerify.d.ts","sourceRoot":"","sources":["../../../src/NativeOtpAutoVerify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,YAAY,IAAI;QAAE,kBAAkB,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1C,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACtC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;;AAED,wBAAuE"}
@@ -0,0 +1,37 @@
1
+ declare const OTP_RECEIVED_EVENT: string;
2
+ export type OtpDigits = 4 | 5 | 6;
3
+ export interface UseOtpVerificationOptions {
4
+ /** Extract OTP with this many digits (4, 5, or 6). OTP is set when SMS is received. */
5
+ numberOfDigits?: OtpDigits;
6
+ }
7
+ export interface UseOtpVerificationResult {
8
+ /** App hash string for SMS (e.g. "uW87Uq6teXc"). Use at end of your OTP message. */
9
+ hashCode: string;
10
+ /** Extracted OTP when numberOfDigits is set and an SMS was received. */
11
+ otp: string | null;
12
+ /** Full SMS text when received. */
13
+ sms: string | null;
14
+ /** True when the 5-minute SMS Retriever timeout occurred. */
15
+ timeoutError: boolean;
16
+ /** Start listening for OTP again (e.g. after timeout or error). */
17
+ startListening: () => Promise<void>;
18
+ /** Stop listening and clean up. Call on unmount. */
19
+ stopListening: () => void;
20
+ }
21
+ export interface OtpListenerSubscription {
22
+ remove: () => void;
23
+ }
24
+ /** Extracts a numeric OTP of 4–6 digits from SMS text. */
25
+ export declare function extractOtp(sms: string, numberOfDigits?: OtpDigits): string | null;
26
+ /** Returns app hash strings for the current app. Android only; iOS returns []. */
27
+ export declare function getHash(): Promise<string[]>;
28
+ /** Starts SMS Retriever and subscribes to OTP events. Returns subscription with remove(). */
29
+ export declare function activateOtpListener(handler: (sms: string, extractedOtp?: string | null) => void, options?: {
30
+ numberOfDigits?: OtpDigits;
31
+ }): Promise<OtpListenerSubscription>;
32
+ /** Stops SMS listening and removes all listeners. */
33
+ export declare function removeListener(): void;
34
+ /** Hook for OTP verification. Stops listener on unmount. */
35
+ export declare function useOtpVerification(options?: UseOtpVerificationOptions): UseOtpVerificationResult;
36
+ export { OTP_RECEIVED_EVENT };
37
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAUA,QAAA,MAAM,kBAAkB,QAET,CAAC;AAKhB,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAQlC,MAAM,WAAW,yBAAyB;IACxC,uFAAuF;IACvF,cAAc,CAAC,EAAE,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,wBAAwB;IACvC,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,mCAAmC;IACnC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,6DAA6D;IAC7D,YAAY,EAAE,OAAO,CAAC;IACtB,mEAAmE;IACnE,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,oDAAoD;IACpD,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,0DAA0D;AAC1D,wBAAgB,UAAU,CACxB,GAAG,EAAE,MAAM,EACX,cAAc,GAAE,SAA0B,GACzC,MAAM,GAAG,IAAI,CAIf;AAED,kFAAkF;AAClF,wBAAsB,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAIjD;AAED,6FAA6F;AAC7F,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,EAC5D,OAAO,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,SAAS,CAAA;CAAE,GACvC,OAAO,CAAC,uBAAuB,CAAC,CAqBlC;AAED,qDAAqD;AACrD,wBAAgB,cAAc,IAAI,IAAI,CAIrC;AAED,4DAA4D;AAC5D,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,yBAA8B,GACtC,wBAAwB,CAyD1B;AAED,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,176 @@
1
+ {
2
+ "name": "react-native-otp-auto-verify",
3
+ "version": "0.1.0",
4
+ "description": "react-native-otp-auto-verify is a React Native library for automatic OTP detection and verification on Android using the Google SMS Retriever API. It enables secure, permission-less OTP auto-reading without requiring READ_SMS, making it fully Play Store compliant.",
5
+ "keywords": [
6
+ "react-native",
7
+ "ios",
8
+ "android"
9
+ ],
10
+ "homepage": "https://github.com/kailas-rathod/react-native-otp-auto-verify#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/kailas-rathod/react-native-otp-auto-verify/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/kailas-rathod/react-native-otp-auto-verify.git"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Kailas Rathod (https://github.com/kailas-rathod)",
20
+ "type": "commonjs",
21
+ "exports": {
22
+ ".": {
23
+ "source": "./src/index.tsx",
24
+ "types": "./lib/typescript/src/index.d.ts",
25
+ "default": "./lib/module/index.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "main": "./lib/module/index.js",
30
+ "types": "./lib/typescript/src/index.d.ts",
31
+ "directories": {
32
+ "example": "example"
33
+ },
34
+ "files": [
35
+ "src",
36
+ "lib",
37
+ "android",
38
+ "ios",
39
+ "cpp",
40
+ "*.podspec",
41
+ "react-native.config.js",
42
+ "!ios/build",
43
+ "!android/build",
44
+ "!android/gradle",
45
+ "!android/gradlew",
46
+ "!android/gradlew.bat",
47
+ "!android/local.properties",
48
+ "!**/__tests__",
49
+ "!**/__fixtures__",
50
+ "!**/__mocks__",
51
+ "!**/.*"
52
+ ],
53
+ "workspaces": [
54
+ "example"
55
+ ],
56
+ "scripts": {
57
+ "example": "yarn workspace react-native-otp-auto-verify-example",
58
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
59
+ "prepare": "bob build",
60
+ "typecheck": "tsc",
61
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
62
+ "test": "jest",
63
+ "release": "release-it --only-version"
64
+ },
65
+ "dependencies": {
66
+
67
+ },
68
+ "devDependencies": {
69
+ "@commitlint/config-conventional": "^19.8.1",
70
+ "@eslint/compat": "^1.3.2",
71
+ "@eslint/eslintrc": "^3.3.1",
72
+ "@eslint/js": "^9.35.0",
73
+ "@react-native/babel-preset": "0.83.0",
74
+ "@react-native/eslint-config": "0.83.0",
75
+ "@release-it/conventional-changelog": "^10.0.1",
76
+ "@types/jest": "^29.5.14",
77
+ "@types/react": "^19.2.0",
78
+ "commitlint": "^19.8.1",
79
+ "del-cli": "^6.0.0",
80
+ "eslint": "^9.35.0",
81
+ "eslint-config-prettier": "^10.1.8",
82
+ "eslint-plugin-prettier": "^5.5.4",
83
+ "jest": "^29.7.0",
84
+ "lefthook": "^2.0.3",
85
+ "prettier": "^2.8.8",
86
+ "react": "19.2.0",
87
+ "react-native": "0.83.0",
88
+ "react-native-builder-bob": "0.39.1",
89
+ "release-it": "^19.0.4",
90
+ "turbo": "^2.5.6",
91
+ "typescript": "^5.9.2"
92
+ },
93
+ "peerDependencies": {
94
+ "react": "*",
95
+ "react-native": "*"
96
+ },
97
+ "publishConfig": {
98
+ "registry": "https://registry.npmjs.org/"
99
+ },
100
+ "packageManager": "yarn@4.11.0",
101
+ "react-native-builder-bob": {
102
+ "source": "src",
103
+ "output": "lib",
104
+ "targets": [
105
+ [
106
+ "module",
107
+ {
108
+ "esm": true
109
+ }
110
+ ],
111
+ [
112
+ "typescript",
113
+ {
114
+ "project": "tsconfig.build.json"
115
+ }
116
+ ]
117
+ ]
118
+ },
119
+ "codegenConfig": {
120
+ "name": "OtpAutoVerifySpec",
121
+ "type": "modules",
122
+ "jsSrcsDir": "src",
123
+ "android": {
124
+ "javaPackageName": "com.otpautoverify"
125
+ }
126
+ },
127
+ "prettier": {
128
+ "quoteProps": "consistent",
129
+ "singleQuote": true,
130
+ "tabWidth": 2,
131
+ "trailingComma": "es5",
132
+ "useTabs": false
133
+ },
134
+ "jest": {
135
+ "preset": "react-native",
136
+ "modulePathIgnorePatterns": [
137
+ "<rootDir>/example/node_modules",
138
+ "<rootDir>/lib/"
139
+ ]
140
+ },
141
+ "commitlint": {
142
+ "extends": [
143
+ "@commitlint/config-conventional"
144
+ ]
145
+ },
146
+ "release-it": {
147
+ "git": {
148
+ "commitMessage": "chore: release ${version}",
149
+ "tagName": "v${version}"
150
+ },
151
+ "npm": {
152
+ "publish": true
153
+ },
154
+ "github": {
155
+ "release": true
156
+ },
157
+ "plugins": {
158
+ "@release-it/conventional-changelog": {
159
+ "preset": {
160
+ "name": "angular"
161
+ }
162
+ }
163
+ }
164
+ },
165
+ "create-react-native-library": {
166
+ "type": "turbo-module",
167
+ "languages": "kotlin-objc",
168
+ "tools": [
169
+ "eslint",
170
+ "jest",
171
+ "lefthook",
172
+ "release-it"
173
+ ],
174
+ "version": "0.57.0"
175
+ }
176
+ }
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {},
5
+ ios: {},
6
+ },
7
+ },
8
+ };
@@ -0,0 +1,11 @@
1
+ import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
+
3
+ export interface Spec extends TurboModule {
4
+ getConstants(): { OTP_RECEIVED_EVENT: string };
5
+ getHash(): Promise<ReadonlyArray<string>>;
6
+ startSmsRetriever(): Promise<boolean>;
7
+ addListener(eventName: string): void;
8
+ removeListeners(count: number): void;
9
+ }
10
+
11
+ export default TurboModuleRegistry.getEnforcing<Spec>('OtpAutoVerify');
package/src/index.tsx ADDED
@@ -0,0 +1,163 @@
1
+ import * as React from 'react';
2
+ import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
3
+ import NativeOtpAutoVerify from './NativeOtpAutoVerify';
4
+
5
+ const { OtpAutoVerify } = NativeModules;
6
+ const eventEmitter =
7
+ Platform.OS === 'android' && OtpAutoVerify
8
+ ? new NativeEventEmitter(OtpAutoVerify)
9
+ : null;
10
+
11
+ const OTP_RECEIVED_EVENT =
12
+ (NativeOtpAutoVerify.getConstants?.()?.OTP_RECEIVED_EVENT as string) ??
13
+ 'otpReceived';
14
+
15
+ const TIMEOUT_MESSAGE = 'Timeout Error.';
16
+ const DEFAULT_DIGITS = 6;
17
+
18
+ export type OtpDigits = 4 | 5 | 6;
19
+
20
+ const OTP_REGEX: Record<OtpDigits, RegExp> = {
21
+ 4: /\b(\d{4})\b/,
22
+ 5: /\b(\d{5})\b/,
23
+ 6: /\b(\d{6})\b/,
24
+ };
25
+
26
+ export interface UseOtpVerificationOptions {
27
+ /** Extract OTP with this many digits (4, 5, or 6). OTP is set when SMS is received. */
28
+ numberOfDigits?: OtpDigits;
29
+ }
30
+
31
+ export interface UseOtpVerificationResult {
32
+ /** App hash string for SMS (e.g. "uW87Uq6teXc"). Use at end of your OTP message. */
33
+ hashCode: string;
34
+ /** Extracted OTP when numberOfDigits is set and an SMS was received. */
35
+ otp: string | null;
36
+ /** Full SMS text when received. */
37
+ sms: string | null;
38
+ /** True when the 5-minute SMS Retriever timeout occurred. */
39
+ timeoutError: boolean;
40
+ /** Start listening for OTP again (e.g. after timeout or error). */
41
+ startListening: () => Promise<void>;
42
+ /** Stop listening and clean up. Call on unmount. */
43
+ stopListening: () => void;
44
+ }
45
+
46
+ export interface OtpListenerSubscription {
47
+ remove: () => void;
48
+ }
49
+
50
+ /** Extracts a numeric OTP of 4–6 digits from SMS text. */
51
+ export function extractOtp(
52
+ sms: string,
53
+ numberOfDigits: OtpDigits = DEFAULT_DIGITS
54
+ ): string | null {
55
+ if (!sms || typeof sms !== 'string') return null;
56
+ const match = sms.match(OTP_REGEX[numberOfDigits]);
57
+ return match ? match[1]! : null;
58
+ }
59
+
60
+ /** Returns app hash strings for the current app. Android only; iOS returns []. */
61
+ export async function getHash(): Promise<string[]> {
62
+ if (Platform.OS !== 'android') return [];
63
+ const arr = await NativeOtpAutoVerify.getHash();
64
+ return Array.from(arr);
65
+ }
66
+
67
+ /** Starts SMS Retriever and subscribes to OTP events. Returns subscription with remove(). */
68
+ export async function activateOtpListener(
69
+ handler: (sms: string, extractedOtp?: string | null) => void,
70
+ options?: { numberOfDigits?: OtpDigits }
71
+ ): Promise<OtpListenerSubscription> {
72
+ if (Platform.OS !== 'android' || !eventEmitter) {
73
+ throw new Error('SMS Retriever is only supported on Android.');
74
+ }
75
+
76
+ const numberOfDigits = options?.numberOfDigits ?? DEFAULT_DIGITS;
77
+ const subscription = eventEmitter.addListener(
78
+ OTP_RECEIVED_EVENT,
79
+ (...args: unknown[]) => {
80
+ const smsText = String(args[0] ?? '');
81
+ handler(smsText, extractOtp(smsText, numberOfDigits));
82
+ }
83
+ );
84
+
85
+ await NativeOtpAutoVerify.startSmsRetriever();
86
+ return {
87
+ remove: () => {
88
+ subscription.remove();
89
+ NativeOtpAutoVerify.removeListeners(0);
90
+ },
91
+ };
92
+ }
93
+
94
+ /** Stops SMS listening and removes all listeners. */
95
+ export function removeListener(): void {
96
+ if (Platform.OS === 'android') {
97
+ NativeOtpAutoVerify.removeListeners(0);
98
+ }
99
+ }
100
+
101
+ /** Hook for OTP verification. Stops listener on unmount. */
102
+ export function useOtpVerification(
103
+ options: UseOtpVerificationOptions = {}
104
+ ): UseOtpVerificationResult {
105
+ const numberOfDigits = options.numberOfDigits ?? DEFAULT_DIGITS;
106
+ const [hashCode, setHashCode] = React.useState('');
107
+ const [otp, setOtp] = React.useState<string | null>(null);
108
+ const [sms, setSms] = React.useState<string | null>(null);
109
+ const [timeoutError, setTimeoutError] = React.useState(false);
110
+ const subscriptionRef = React.useRef<OtpListenerSubscription | null>(null);
111
+
112
+ const stopListening = React.useCallback(() => {
113
+ subscriptionRef.current?.remove();
114
+ subscriptionRef.current = null;
115
+ removeListener();
116
+ }, []);
117
+
118
+ const startListening = React.useCallback(async () => {
119
+ if (Platform.OS !== 'android') return;
120
+ setOtp(null);
121
+ setSms(null);
122
+ setTimeoutError(false);
123
+ try {
124
+ const hashes = await getHash();
125
+ setHashCode(hashes[0] ?? '');
126
+ } catch {
127
+ // getHash failed; continue to start listener
128
+ }
129
+ try {
130
+ const sub = await activateOtpListener(
131
+ (smsText: string, extractedOtp: string | null | undefined) => {
132
+ setSms(smsText);
133
+ if (smsText === TIMEOUT_MESSAGE) {
134
+ setTimeoutError(true);
135
+ return;
136
+ }
137
+ if (extractedOtp) setOtp(extractedOtp);
138
+ },
139
+ { numberOfDigits }
140
+ );
141
+ subscriptionRef.current = sub;
142
+ } catch {
143
+ subscriptionRef.current = null;
144
+ throw new Error('Failed to start OTP listener');
145
+ }
146
+ }, [numberOfDigits]);
147
+
148
+ React.useEffect(() => () => stopListening(), [stopListening]);
149
+
150
+ return React.useMemo(
151
+ () => ({
152
+ hashCode,
153
+ otp,
154
+ sms,
155
+ timeoutError,
156
+ startListening,
157
+ stopListening,
158
+ }),
159
+ [hashCode, otp, sms, timeoutError, startListening, stopListening]
160
+ );
161
+ }
162
+
163
+ export { OTP_RECEIVED_EVENT };