react-native-otp-auto-verify 0.1.9 → 0.2.1

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/package.json CHANGED
@@ -1,175 +1,183 @@
1
- {
2
- "name": "react-native-otp-auto-verify",
3
- "version": "0.1.9",
4
- "description": "react-native-otp-auto-verify is a React Native library for automatic OTP verification on Android and iOS. It uses the Google SMS Retriever API to detect OTP codes automatically without requiring the READ_SMS permission. The library is fully Play Store compliant, improves user login experience, supports TurboModule, and works with the React Native New Architecture",
5
- "keywords": [
6
- "react-native",
7
- "react-native-library",
8
- "react-native-otp",
9
- "react-native-otp-auto-verify",
10
- "react-native-sms",
11
- "otp",
12
- "otp-verification",
13
- "otp-auto-verify",
14
- "otp-auto-read",
15
- "sms",
16
- "sms-retriever",
17
- "google-sms-retriever",
18
- "android-otp",
19
- "ios-otp",
20
- "otp-without-permission",
21
- "play-store-compliant",
22
- "react-native-new-architecture",
23
- "turbo-module"
24
- ],
25
- "homepage": "https://github.com/kailas-rathod/react-native-otp-auto-verify?tab=readme-ov-file",
26
- "bugs": {
27
- "url": "https://github.com/kailas-rathod/react-native-otp-auto-verify/issues"
28
- },
29
- "repository": {
30
- "type": "git",
31
- "url": "git+https://github.com/kailas-rathod/react-native-otp-auto-verify.git"
32
- },
33
- "license": "MIT",
34
- "author": "Kailas Rathod (https://github.com/kailas-rathod)",
35
- "type": "commonjs",
36
- "main": "./lib/module/index.js",
37
- "types": "./lib/typescript/src/index.d.ts",
38
- "files": [
39
- "src",
40
- "lib",
41
- "android",
42
- "ios",
43
- "*.podspec",
44
- "react-native.config.js",
45
- "!ios/build",
46
- "!android/build",
47
- "!android/gradle",
48
- "!android/gradlew",
49
- "!android/gradlew.bat",
50
- "!android/local.properties",
51
- "!**/__tests__",
52
- "!**/__fixtures__",
53
- "!**/__mocks__",
54
- "!**/.*"
55
- ],
56
- "scripts": {
57
- "prepare": "bob build",
58
- "typecheck": "tsc",
59
- "lint": "eslint \"**/*.{js,ts,tsx}\"",
60
- "test": "jest",
61
- "check": "yarn typecheck && yarn lint && yarn test",
62
- "release": "release-it --only-version"
63
- },
64
- "devDependencies": {
65
- "@commitlint/config-conventional": "^19.8.1",
66
- "@eslint/compat": "^1.3.2",
67
- "@eslint/eslintrc": "^3.3.4",
68
- "@eslint/js": "^9.35.0",
69
- "@react-native/babel-preset": "0.83.0",
70
- "@react-native-community/eslint-config": "^3.2.0",
71
- "@release-it/conventional-changelog": "^10.0.1",
72
- "@types/jest": "^29.5.14",
73
- "@types/react": "^19.2.0",
74
- "commitlint": "^19.8.1",
75
- "eslint": "^10.0.2",
76
- "eslint-config-prettier": "^10.1.8",
77
- "eslint-plugin-prettier": "^5.5.4",
78
- "eslint-plugin-react-native": "^5.0.0",
79
- "jest": "^30.2.0",
80
- "prettier": "^2.8.8",
81
- "react": "19.2.4",
82
- "react-native": "0.83.0",
83
- "react-native-builder-bob": "0.39.1",
84
- "release-it": "^19.0.4",
85
- "typescript": "^5.9.3"
86
- },
87
- "peerDependencies": {
88
- "react": "*",
89
- "react-native": "*"
90
- },
91
- "publishConfig": {
92
- "registry": "https://registry.npmjs.org/"
93
- },
94
- "eslintConfig": {
95
- "root": true,
96
- "extends": [
97
- "@react-native-community",
98
- "prettier"
99
- ],
100
- "rules": {
101
- "prettier/prettier": [
102
- "error",
103
- {
104
- "quoteProps": "consistent",
105
- "singleQuote": true,
106
- "tabWidth": 2,
107
- "trailingComma": "es5",
108
- "useTabs": false
109
- }
110
- ]
111
- }
112
- },
113
- "eslintIgnore": [
114
- "node_modules/",
115
- "lib/"
116
- ],
117
- "packageManager": "yarn@4.11.0",
118
- "react-native-builder-bob": {
119
- "source": "src",
120
- "output": "lib",
121
- "targets": [
122
- [
123
- "module",
124
- {
125
- "esm": true
126
- }
127
- ],
128
- [
129
- "typescript",
130
- {
131
- "project": "tsconfig.build.json"
132
- }
133
- ]
134
- ]
135
- },
136
- "codegenConfig": {
137
- "name": "OtpAutoVerifySpec",
138
- "type": "modules",
139
- "jsSrcsDir": "src",
140
- "android": {
141
- "javaPackageName": "com.otpautoverify"
142
- }
143
- },
144
- "prettier": {
145
- "quoteProps": "consistent",
146
- "singleQuote": true,
147
- "tabWidth": 2,
148
- "trailingComma": "es5",
149
- "useTabs": false
150
- },
151
- "commitlint": {
152
- "extends": [
153
- "@commitlint/config-conventional"
154
- ]
155
- },
156
- "release-it": {
157
- "git": {
158
- "commitMessage": "chore: release ${version}",
159
- "tagName": "v${version}"
160
- },
161
- "npm": {
162
- "publish": true
163
- },
164
- "github": {
165
- "release": true
166
- },
167
- "plugins": {
168
- "@release-it/conventional-changelog": {
169
- "preset": {
170
- "name": "angular"
171
- }
172
- }
173
- }
174
- }
175
- }
1
+ {
2
+ "name": "react-native-otp-auto-verify",
3
+ "version": "0.2.1",
4
+ "description": "react-native-otp-auto-verify is a React Native library for automatic OTP verification on Android and iOS. It uses the Google SMS Retriever API to detect OTP codes automatically without requiring the READ_SMS permission. The library is fully Play Store compliant, improves user login experience, supports TurboModule, and works with the React Native New Architecture",
5
+ "keywords": [
6
+ "react-native",
7
+ "react-native-library",
8
+ "react-native-otp",
9
+ "react-native-otp-auto-verify",
10
+ "react-native-sms",
11
+ "otp",
12
+ "otp-verification",
13
+ "otp-auto-verify",
14
+ "otp-auto-read",
15
+ "sms",
16
+ "sms-retriever",
17
+ "google-sms-retriever",
18
+ "android-otp",
19
+ "ios-otp",
20
+ "otp-without-permission",
21
+ "play-store-compliant",
22
+ "react-native-new-architecture",
23
+ "turbo-module"
24
+ ],
25
+ "homepage": "https://github.com/kailas-rathod/react-native-otp-auto-verify?tab=readme-ov-file",
26
+ "bugs": {
27
+ "url": "https://github.com/kailas-rathod/react-native-otp-auto-verify/issues"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/kailas-rathod/react-native-otp-auto-verify.git"
32
+ },
33
+ "license": "MIT",
34
+ "author": "Kailas Rathod (https://github.com/kailas-rathod)",
35
+ "type": "commonjs",
36
+ "main": "./lib/module/index.js",
37
+ "types": "./lib/typescript/src/index.d.ts",
38
+ "files": [
39
+ "src",
40
+ "lib",
41
+ "android",
42
+ "ios",
43
+ "*.podspec",
44
+ "react-native.config.js",
45
+ "!ios/build",
46
+ "!android/build",
47
+ "!android/gradle",
48
+ "!android/gradlew",
49
+ "!android/gradlew.bat",
50
+ "!android/local.properties",
51
+ "!**/__tests__",
52
+ "!**/__fixtures__",
53
+ "!**/__mocks__",
54
+ "!**/.*"
55
+ ],
56
+ "scripts": {
57
+ "prepare": "bob build",
58
+ "typecheck": "tsc",
59
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
60
+ "test": "jest",
61
+ "check": "yarn typecheck && yarn lint && yarn test",
62
+ "release": "release-it --only-version"
63
+ },
64
+ "devDependencies": {
65
+ "@babel/eslint-parser": "^7.28.0",
66
+ "@commitlint/config-conventional": "^19.8.1",
67
+ "@eslint/eslintrc": "^3.3.4",
68
+ "@eslint/js": "9.35.0",
69
+ "@react-native-community/eslint-config": "^3.2.0",
70
+ "@react-native-community/eslint-plugin": "^1.1.0",
71
+ "@typescript-eslint/eslint-plugin": "^8.15.0",
72
+ "@typescript-eslint/parser": "^8.15.0",
73
+ "@react-native/babel-preset": "0.83.3",
74
+ "@release-it/conventional-changelog": "^10.0.6",
75
+ "@types/jest": "^29.5.14",
76
+ "@types/react": "^19.2.0",
77
+ "commitlint": "^19.8.1",
78
+ "eslint": "9.35.0",
79
+ "eslint-config-prettier": "^9.1.0",
80
+ "eslint-plugin-eslint-comments": "^3.2.0",
81
+ "eslint-plugin-ft-flow": "^2.0.1",
82
+ "eslint-plugin-jest": "^28.11.0",
83
+ "eslint-plugin-prettier": "^5.5.4",
84
+ "eslint-plugin-react": "^7.37.0",
85
+ "eslint-plugin-react-hooks": "^4.6.0",
86
+ "eslint-plugin-react-native": "^5.0.0",
87
+ "jest": "^30.3.0",
88
+ "prettier": "^3.4.2",
89
+ "react": "19.2.5",
90
+ "react-native": "0.83.3",
91
+ "react-native-builder-bob": "0.39.1",
92
+ "release-it": "^19.0.4",
93
+ "typescript": "^5.9.3"
94
+ },
95
+ "peerDependencies": {
96
+ "react": "*",
97
+ "react-native": "*"
98
+ },
99
+ "publishConfig": {
100
+ "registry": "https://registry.npmjs.org/"
101
+ },
102
+ "eslintConfig": {
103
+ "root": true,
104
+ "extends": [
105
+ "@react-native-community",
106
+ "prettier"
107
+ ],
108
+ "rules": {
109
+ "prettier/prettier": [
110
+ "error",
111
+ {
112
+ "quoteProps": "consistent",
113
+ "singleQuote": true,
114
+ "tabWidth": 2,
115
+ "trailingComma": "es5",
116
+ "useTabs": false
117
+ }
118
+ ]
119
+ }
120
+ },
121
+ "eslintIgnore": [
122
+ "node_modules/",
123
+ "lib/"
124
+ ],
125
+ "packageManager": "yarn@4.11.0",
126
+ "react-native-builder-bob": {
127
+ "source": "src",
128
+ "output": "lib",
129
+ "targets": [
130
+ [
131
+ "module",
132
+ {
133
+ "esm": true
134
+ }
135
+ ],
136
+ [
137
+ "typescript",
138
+ {
139
+ "project": "tsconfig.build.json"
140
+ }
141
+ ]
142
+ ]
143
+ },
144
+ "codegenConfig": {
145
+ "name": "OtpAutoVerifySpec",
146
+ "type": "modules",
147
+ "jsSrcsDir": "src",
148
+ "android": {
149
+ "javaPackageName": "com.otpautoverify"
150
+ }
151
+ },
152
+ "prettier": {
153
+ "quoteProps": "consistent",
154
+ "singleQuote": true,
155
+ "tabWidth": 2,
156
+ "trailingComma": "es5",
157
+ "useTabs": false
158
+ },
159
+ "commitlint": {
160
+ "extends": [
161
+ "@commitlint/config-conventional"
162
+ ]
163
+ },
164
+ "release-it": {
165
+ "git": {
166
+ "commitMessage": "chore: release ${version}",
167
+ "tagName": "v${version}"
168
+ },
169
+ "npm": {
170
+ "publish": true
171
+ },
172
+ "github": {
173
+ "release": true
174
+ },
175
+ "plugins": {
176
+ "@release-it/conventional-changelog": {
177
+ "preset": {
178
+ "name": "angular"
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
@@ -1,6 +1,10 @@
1
- import { TurboModuleRegistry, type TurboModule } from 'react-native';
1
+ import {
2
+ NativeModules,
3
+ TurboModuleRegistry,
4
+ type TurboModule,
5
+ } from 'react-native';
2
6
 
3
- export interface OtpAutoVerifySpec extends TurboModule {
7
+ export interface Spec extends TurboModule {
4
8
  getConstants(): { OTP_RECEIVED_EVENT: string };
5
9
  getHash(): Promise<ReadonlyArray<string>>;
6
10
  startSmsRetriever(): Promise<boolean>;
@@ -8,6 +12,26 @@ export interface OtpAutoVerifySpec extends TurboModule {
8
12
  removeListeners(count: number): void;
9
13
  }
10
14
 
11
- export default TurboModuleRegistry.getEnforcing<OtpAutoVerifySpec>(
12
- 'OtpAutoVerify'
13
- );
15
+ function loadNativeModule(): Spec {
16
+ try {
17
+ return TurboModuleRegistry.getEnforcing<Spec>('OtpAutoVerify');
18
+ } catch {
19
+ const legacy = NativeModules.OtpAutoVerify;
20
+ if (!legacy) {
21
+ throw new Error(
22
+ 'OtpAutoVerify native module is not available. Ensure the library is properly linked.'
23
+ );
24
+ }
25
+ return legacy as Spec;
26
+ }
27
+ }
28
+
29
+ let cachedModule: Spec | null = null;
30
+
31
+ /** Resolves the native module on first use so importing this package does not throw before APIs run. */
32
+ export function getOtpNativeModule(): Spec {
33
+ if (cachedModule === null) {
34
+ cachedModule = loadNativeModule();
35
+ }
36
+ return cachedModule;
37
+ }
package/src/index.tsx CHANGED
@@ -1,30 +1,45 @@
1
1
  import * as React from 'react';
2
- import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
3
- import NativeOtpAutoVerify from './NativeOtpAutoVerify';
2
+ import { NativeEventEmitter, Platform } from 'react-native';
3
+ import { getOtpNativeModule } from './NativeOtpAutoVerify';
4
4
 
5
- const { OtpAutoVerify } = NativeModules;
6
- const eventEmitter =
7
- Platform.OS === 'android' && OtpAutoVerify
8
- ? new NativeEventEmitter(OtpAutoVerify)
9
- : null;
5
+ /** Must match native `OTP_RECEIVED_EVENT` (Android/iOS). */
6
+ export const OTP_RECEIVED_EVENT = 'otpReceived';
10
7
 
11
- const OTP_RECEIVED_EVENT =
12
- (NativeOtpAutoVerify.getConstants?.()?.OTP_RECEIVED_EVENT as string) ??
13
- 'otpReceived';
14
-
15
- const TIMEOUT_MESSAGE = 'Timeout Error.';
8
+ export const TIMEOUT_MESSAGE = 'Timeout Error.';
16
9
  const DEFAULT_DIGITS = 6;
17
10
 
18
- export type OtpDigits = 4 | 5 | 6;
11
+ const MIN_OTP_DIGITS = 4;
12
+ const MAX_OTP_DIGITS = 8;
19
13
 
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
- };
14
+ export type OtpDigits = 4 | 5 | 6 | 7 | 8;
15
+
16
+ let androidEventEmitter: NativeEventEmitter | null | undefined;
17
+
18
+ function getAndroidEventEmitter(): NativeEventEmitter | null {
19
+ if (Platform.OS !== 'android') return null;
20
+ if (androidEventEmitter !== undefined) {
21
+ return androidEventEmitter;
22
+ }
23
+ try {
24
+ androidEventEmitter = new NativeEventEmitter(getOtpNativeModule());
25
+ return androidEventEmitter;
26
+ } catch {
27
+ androidEventEmitter = null;
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function getOtpRegex(digits: number): RegExp {
33
+ const d =
34
+ digits >= MIN_OTP_DIGITS && digits <= MAX_OTP_DIGITS
35
+ ? digits
36
+ : DEFAULT_DIGITS;
37
+ // Prefer non-`\b` boundaries so OTPs still match after ":" and similar (e.g. "OTP:123456").
38
+ return new RegExp(`(^|[^0-9])(\\d{${d}})(?!\\d)`);
39
+ }
25
40
 
26
41
  export interface UseOtpVerificationOptions {
27
- /** Extract OTP with this many digits (4, 5, or 6). OTP is set when SMS is received. */
42
+ /** Extract OTP with this many digits (4–8). OTP is set when SMS is received. */
28
43
  numberOfDigits?: OtpDigits;
29
44
  }
30
45
 
@@ -50,8 +65,7 @@ export interface OtpListenerSubscription {
50
65
  }
51
66
 
52
67
  /**
53
- * Extracts a numeric OTP of 4, 5, or 6 digits from SMS text.
54
- * Only these lengths are supported (numberOfDigits: 4 | 5 | 6).
68
+ * Extracts a numeric OTP of 4–8 digits from SMS text.
55
69
  */
56
70
  export function extractOtp(
57
71
  sms: string,
@@ -60,28 +74,39 @@ export function extractOtp(
60
74
  if (!sms || typeof sms !== 'string') return null;
61
75
  const trimmed = sms.trim();
62
76
  if (!trimmed) return null;
63
- const match = trimmed.match(OTP_REGEX[numberOfDigits]);
64
- return match ? match[1]! : null;
77
+ const regex = getOtpRegex(numberOfDigits);
78
+ const match = trimmed.match(regex);
79
+ return match ? match[2]! : null;
65
80
  }
66
81
 
67
82
  /** Returns app hash strings for the current app. Android only; iOS returns []. */
68
83
  export async function getHash(): Promise<string[]> {
69
84
  if (Platform.OS !== 'android') return [];
70
- const arr = await NativeOtpAutoVerify.getHash();
85
+ const arr = await getOtpNativeModule().getHash();
71
86
  return Array.from(arr);
72
87
  }
73
88
 
74
- /** Starts SMS Retriever and subscribes to OTP events. Returns subscription with remove(). */
89
+ /** No-op subscription for platforms where SMS Retriever is not supported (e.g. iOS). */
90
+ const NOOP_SUBSCRIPTION: OtpListenerSubscription = {
91
+ remove: () => {},
92
+ };
93
+
94
+ /** Starts SMS Retriever and subscribes to OTP events. Returns subscription with remove(). On iOS, returns no-op (call remove() safely). */
75
95
  export async function activateOtpListener(
76
96
  handler: (sms: string, extractedOtp?: string | null) => void,
77
97
  options?: { numberOfDigits?: OtpDigits }
78
98
  ): Promise<OtpListenerSubscription> {
79
- if (Platform.OS !== 'android' || !eventEmitter) {
80
- throw new Error('SMS Retriever is only supported on Android.');
99
+ if (Platform.OS !== 'android') {
100
+ return NOOP_SUBSCRIPTION;
101
+ }
102
+
103
+ const emitter = getAndroidEventEmitter();
104
+ if (!emitter) {
105
+ return NOOP_SUBSCRIPTION;
81
106
  }
82
107
 
83
108
  const numberOfDigits = options?.numberOfDigits ?? DEFAULT_DIGITS;
84
- const subscription = eventEmitter.addListener(
109
+ const subscription = emitter.addListener(
85
110
  OTP_RECEIVED_EVENT,
86
111
  (...args: unknown[]) => {
87
112
  const smsText = String(args[0] ?? '');
@@ -89,11 +114,21 @@ export async function activateOtpListener(
89
114
  }
90
115
  );
91
116
 
92
- await NativeOtpAutoVerify.startSmsRetriever();
117
+ try {
118
+ await getOtpNativeModule().startSmsRetriever();
119
+ } catch (err) {
120
+ subscription.remove();
121
+ throw err;
122
+ }
123
+
93
124
  return {
94
125
  remove: () => {
95
126
  subscription.remove();
96
- NativeOtpAutoVerify.removeListeners(0);
127
+ try {
128
+ getOtpNativeModule().removeListeners(0);
129
+ } catch {
130
+ // Native may be unavailable during teardown
131
+ }
97
132
  },
98
133
  };
99
134
  }
@@ -103,8 +138,11 @@ export async function activateOtpListener(
103
138
  * The native module ignores the count parameter and always unregisters the SMS receiver.
104
139
  */
105
140
  export function removeListener(): void {
106
- if (Platform.OS === 'android') {
107
- NativeOtpAutoVerify.removeListeners(0);
141
+ if (Platform.OS !== 'android') return;
142
+ try {
143
+ getOtpNativeModule().removeListeners(0);
144
+ } catch {
145
+ // Native not linked or already torn down
108
146
  }
109
147
  }
110
148
 
@@ -119,26 +157,37 @@ export function useOtpVerification(
119
157
  const [timeoutError, setTimeoutError] = React.useState(false);
120
158
  const [error, setError] = React.useState<Error | null>(null);
121
159
  const subscriptionRef = React.useRef<OtpListenerSubscription | null>(null);
160
+ const isStartingRef = React.useRef(false);
122
161
 
123
162
  const stopListening = React.useCallback(() => {
124
163
  subscriptionRef.current?.remove();
125
164
  subscriptionRef.current = null;
165
+ isStartingRef.current = false;
126
166
  removeListener();
127
167
  }, []);
128
168
 
129
169
  const startListening = React.useCallback(async () => {
130
170
  if (Platform.OS !== 'android') return;
171
+ if (isStartingRef.current) return;
172
+
173
+ isStartingRef.current = true;
174
+ subscriptionRef.current?.remove();
175
+ subscriptionRef.current = null;
131
176
  setOtp(null);
132
177
  setSms(null);
133
178
  setTimeoutError(false);
134
179
  setError(null);
180
+
135
181
  try {
136
- const hashes = await getHash();
137
- setHashCode(hashes[0] ?? '');
138
- } catch (err) {
139
- setError(err instanceof Error ? err : new Error(String(err)));
140
- }
141
- try {
182
+ try {
183
+ const hashes = await getHash();
184
+ setHashCode(hashes[0] ?? '');
185
+ } catch (err) {
186
+ const wrapped = err instanceof Error ? err : new Error(String(err));
187
+ setError(wrapped);
188
+ return;
189
+ }
190
+
142
191
  const sub = await activateOtpListener(
143
192
  (smsText, extractedOtp) => {
144
193
  setSms(smsText);
@@ -156,6 +205,8 @@ export function useOtpVerification(
156
205
  const wrapped = new Error('Failed to start OTP listener', { cause: err });
157
206
  setError(wrapped);
158
207
  throw wrapped;
208
+ } finally {
209
+ isStartingRef.current = false;
159
210
  }
160
211
  }, [numberOfDigits]);
161
212
 
@@ -174,5 +225,3 @@ export function useOtpVerification(
174
225
  [hashCode, otp, sms, timeoutError, error, startListening, stopListening]
175
226
  );
176
227
  }
177
-
178
- export { OTP_RECEIVED_EVENT };