react-native-otp-auto-verify 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,13 +4,15 @@
4
4
  [![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) [![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) [![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) [![typescript](https://img.shields.io/badge/TypeScript-Ready-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
5
5
 
6
6
  **react-native-otp-auto-verify** is a lightweight and secure React Native OTP auto-verification library for Android, built on the official Google SMS Retriever API. It enables automatic OTP detection without requiring READ_SMS or RECEIVE_SMS permissions, ensuring full Google Play Store compliance and enhanced user trust. Designed for modern authentication flows, this library is ideal for fintech apps, banking applications, e-commerce platforms, and secure login systems.
7
+ No Permissions: It requires zero SMS permissions from the user, making it compliant with strict Google Play Store policies
7
8
 
8
9
  With minimal dependencies and clean architecture, it integrates seamlessly into both React Native Old Architecture and the New Architecture (TurboModule) environments. The solution improves user experience by eliminating manual OTP entry on Android while maintaining strong server-side validation standards.
9
10
 
10
11
  Works best for onboarding/login flows in **banking**, **fintech**, and authentication-heavy apps.
11
12
  Supports both RN Old Architecture and React Native **New Architecture** (TurboModule).
12
13
  **Android**: automatic OTP detection.
13
- **iOS**: auto-OTP is not supported—use manual OTP entry as fallback.
14
+
15
+ **iOS**: Uses iOS Security Code AutoFill (manual tap required).
14
16
 
15
17
  **Connect:** [GitHub](https://github.com/kailas-rathod/react-native-otp-auto-verify) · [npm](https://www.npmjs.com/package/react-native-otp-auto-verify) · [Issues](https://github.com/kailas-rathod/react-native-otp-auto-verify/issues) · [License](https://github.com/kailas-rathod/react-native-otp-auto-verify/blob/main/LICENSE)
16
18
 
@@ -44,9 +46,13 @@ Supports both RN Old Architecture and React Native **New Architecture** (TurboMo
44
46
 
45
47
  ```sh
46
48
  npm install react-native-otp-auto-verify
49
+ ```
47
50
  # or
51
+ ```sh
48
52
  yarn add react-native-otp-auto-verify
53
+ ```
49
54
  # or
55
+ ```sh
50
56
  pnpm add react-native-otp-auto-verify
51
57
  ```
52
58
 
@@ -133,6 +139,150 @@ sub.remove();
133
139
  // or
134
140
  removeListener();
135
141
  ```
142
+ 🔹 Step 1 – Start OTP Listener
143
+ ```ts
144
+ import { useOtpVerification } from 'react-native-otp-auto-verify';
145
+
146
+ const { startOtpListener, stopListener, otp } = useOtpVerification();
147
+
148
+ useEffect(() => {
149
+ startOtpListener();
150
+
151
+ return () => stopListener();
152
+ }, []);
153
+ ```
154
+
155
+ ## Create OTP Screen (Recommended Hook Method)
156
+ ```ts
157
+ import React, { useEffect, useState } from 'react';
158
+ import {
159
+ View,
160
+ Text,
161
+ TextInput,
162
+ Button,
163
+ Platform,
164
+ } from 'react-native';
165
+ import { useOtpVerification } from 'react-native-otp-auto-verify';
166
+
167
+ const OtpScreen = () => {
168
+ const [otpValue, setOtpValue] = useState('');
169
+
170
+ const {
171
+ otp,
172
+ hashCode,
173
+ timeoutError,
174
+ error,
175
+ startListening,
176
+ stopListening,
177
+ } = useOtpVerification({ numberOfDigits: 6 });
178
+
179
+ // Start listener when screen opens
180
+ useEffect(() => {
181
+ if (Platform.OS === 'android') {
182
+ startListening();
183
+ }
184
+
185
+ return () => {
186
+ stopListening();
187
+ };
188
+ }, []);
189
+
190
+ // Auto verify when OTP received
191
+ useEffect(() => {
192
+ if (otp) {
193
+ setOtpValue(otp);
194
+ verifyOtp(otp);
195
+ }
196
+ }, [otp]);
197
+
198
+ const verifyOtp = async (code: string) => {
199
+ console.log('Verifying OTP:', code);
200
+
201
+ // Call your backend API here
202
+ // await api.post('/verify-otp', { otp: code })
203
+ };
204
+
205
+ return (
206
+ <View style={{ padding: 20 }}>
207
+ <Text>Enter OTP</Text>
208
+
209
+ <TextInput
210
+ value={otpValue}
211
+ onChangeText={setOtpValue}
212
+ keyboardType="number-pad"
213
+ maxLength={6}
214
+ textContentType="oneTimeCode"
215
+ autoComplete="sms-otp"
216
+ style={{
217
+ borderWidth: 1,
218
+ padding: 12,
219
+ marginVertical: 12,
220
+ }}
221
+ />
222
+
223
+ <Button title="Verify" onPress={() => verifyOtp(otpValue)} />
224
+
225
+ {timeoutError && (
226
+ <Text style={{ color: 'red' }}>
227
+ Timeout. Please resend OTP.
228
+ </Text>
229
+ )}
230
+
231
+ {error && (
232
+ <Text style={{ color: 'red' }}>
233
+ Error: {error.message}
234
+ </Text>
235
+ )}
236
+ </View>
237
+ );
238
+ };
239
+
240
+ export default OtpScreen;
241
+ ```
242
+
243
+ # Start OTP Listener in Screen
244
+
245
+ ```ts
246
+ import React, { useEffect, useState } from 'react';
247
+ import { View, TextInput, Text } from 'react-native';
248
+ import { useOtpVerification } from 'react-native-otp-auto-verify';
249
+
250
+ export default function OtpScreen() {
251
+ const [otpValue, setOtpValue] = useState('');
252
+
253
+ const {
254
+ otp,
255
+ startListening,
256
+ stopListening,
257
+ } = useOtpVerification({ numberOfDigits: 6 });
258
+
259
+ useEffect(() => {
260
+ startListening(); // Start listening
261
+
262
+ return () => {
263
+ stopListening(); // Cleanup
264
+ };
265
+ }, []);
266
+
267
+ useEffect(() => {
268
+ if (otp) {
269
+ setOtpValue(otp); // OTP automatically retrieved here
270
+ console.log('Retrieved OTP:', otp);
271
+ }
272
+ }, [otp]);
273
+
274
+ return (
275
+ <View>
276
+ <TextInput
277
+ value={otpValue}
278
+ onChangeText={setOtpValue}
279
+ keyboardType="number-pad"
280
+ maxLength={6}
281
+ />
282
+ </View>
283
+ );
284
+ }
285
+ ```
136
286
 
137
287
  # iOS OTP AutoFill (Native)
138
288
 
@@ -233,6 +383,22 @@ The New Architecture (also known as Fabric + TurboModules) is React Native's new
233
383
  - Synchronous native module calls
234
384
  - Improved interoperability with native code
235
385
 
386
+
387
+
388
+
389
+ ## ✨ Feature Comparison
390
+
391
+ | Feature | react-native-otp-auto-verify | Other Packages/Libraries (react-native-otp-auto-verify)|
392
+ |---------------------------|------------------------------|---------------------------|
393
+ | SMS Retriever API | ✅ Yes | ✅ Yes |
394
+ | Requires SMS Permission | ❌ No | ❌ No |
395
+ | TurboModule Support | ✅ Yes | ❌ Usually No |
396
+ | TypeScript Support | ✅ Full | ⚠️ Partial |
397
+ | Hook API | ✅ `useOtpVerification` | ❌ Not Available |
398
+ | App Hash Utility | ✅ Built-in | ⚠️ Basic |
399
+ | Architecture Ready | ✅ Old + New | ⚠️ Mostly Old Only |
400
+ | Maintenance | ✅ Actively Maintained | ⚠️ Varies |
401
+
236
402
  ### Enabling New Architecture
237
403
 
238
404
  The library works automatically with both **Old Architecture** and **New Architecture**. No code changes needed.
@@ -1,80 +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
- }
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
+ }
@@ -1,146 +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
- }
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
+ }