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 +20 -0
- package/OtpAutoVerify.podspec +20 -0
- package/README.md +258 -0
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/otpautoverify/AppSignatureHelper.kt +80 -0
- package/android/src/main/java/com/otpautoverify/OtpAutoVerifyModule.kt +146 -0
- package/android/src/main/java/com/otpautoverify/OtpAutoVerifyPackage.kt +33 -0
- package/android/src/main/java/com/otpautoverify/SmsRetrieverBroadcastReceiver.kt +61 -0
- package/ios/OtpAutoVerify.h +5 -0
- package/ios/OtpAutoVerify.mm +36 -0
- package/lib/module/NativeOtpAutoVerify.ts +11 -0
- package/lib/module/index.js +110 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeOtpAutoVerify.d.ts +13 -0
- package/lib/typescript/src/NativeOtpAutoVerify.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +37 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +176 -0
- package/react-native.config.js +8 -0
- package/src/NativeOtpAutoVerify.ts +11 -0
- package/src/index.tsx +163 -0
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
|
+
[](https://www.npmjs.com/package/react-native-otp-auto-verify)
|
|
6
|
+
[](https://www.npmjs.com/package/react-native-otp-auto-verify)
|
|
7
|
+
[](https://github.com/kailas-rathod/react-native-otp-auto-verify/blob/main/LICENSE)
|
|
8
|
+
[](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,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,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,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 };
|