expo-android-otp-autofill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -0
- package/android/build.gradle +20 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/expo/modules/androidotpautofill/AppSignatureHelper.kt +51 -0
- package/android/src/main/java/expo/modules/androidotpautofill/ExpoAndroidOtpAutofillModule.kt +177 -0
- package/android/src/main/java/expo/modules/androidotpautofill/ExpoAndroidOtpAutofillView.kt +30 -0
- package/build/ExpoAndroidOtpAutofill.d.ts +3 -0
- package/build/ExpoAndroidOtpAutofill.d.ts.map +1 -0
- package/build/ExpoAndroidOtpAutofill.types.d.ts +27 -0
- package/build/ExpoAndroidOtpAutofill.types.d.ts.map +1 -0
- package/build/ExpoAndroidOtpAutofillView.d.ts +4 -0
- package/build/ExpoAndroidOtpAutofillView.d.ts.map +1 -0
- package/build/OtpAutofill.d.ts +65 -0
- package/build/OtpAutofill.d.ts.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/useOtpVerify.d.ts +30 -0
- package/build/useOtpVerify.d.ts.map +1 -0
- package/expo-module.config.json +6 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# expo-android-otp-autofill
|
|
2
|
+
|
|
3
|
+
Android-only Expo module that auto-detects OTP from SMS (READ_SMS and native code included). No third-party OTP library required.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Android only** — READ_SMS permission and native OTP reader; use `textContentType="oneTimeCode"` on iOS.
|
|
8
|
+
- **Expo module** — Native code in the package (autolinked); READ_SMS and Play Services Auth are declared in the module.
|
|
9
|
+
- **Event-based** — Native module emits `onOtpReceived` when an OTP (4–8 digits, configurable) is found.
|
|
10
|
+
- **Two modes** — Use **READ_SMS** (any SMS format) or **SMS Retriever API** (no permission; message must include your app hash).
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx expo install expo-android-otp-autofill
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
### 1. Prebuild (native module is included when you install the package)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx expo prebuild --platform android
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. Use the API in your verify-otp screen
|
|
27
|
+
|
|
28
|
+
**Recommended – useOtpVerify** (SMS Retriever, no permission):
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { useOtpVerify } from 'expo-android-otp-autofill';
|
|
32
|
+
|
|
33
|
+
const { hash, otp, message, timeoutError, startListener, stopListener } = useOtpVerify({
|
|
34
|
+
numberOfDigits: 4,
|
|
35
|
+
onOtpReceived: (otp) => setValue('user_entered_otp', otp, { shouldValidate: true }),
|
|
36
|
+
});
|
|
37
|
+
// hash for your SMS template; otp/message when SMS arrives; timeoutError for "Resend".
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**READ_SMS mode (imperative API):**
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import {
|
|
44
|
+
addOtpListener,
|
|
45
|
+
startOtpListener,
|
|
46
|
+
stopOtpListener,
|
|
47
|
+
} from 'expo-android-otp-autofill';
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const remove = addOtpListener((otp) => setValue('user_entered_otp', otp, { shouldValidate: true }));
|
|
51
|
+
startOtpListener({ length: 6 });
|
|
52
|
+
return () => {
|
|
53
|
+
stopOtpListener();
|
|
54
|
+
remove();
|
|
55
|
+
};
|
|
56
|
+
}, []);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
| Name | Description |
|
|
62
|
+
|------|-------------|
|
|
63
|
+
| `useOtpVerify(options)` | Hook (SMS Retriever): `numberOfDigits`, `onOtpReceived`; returns `hash`, `otp`, `message`, `timeoutError`, `startListener`, `stopListener`. |
|
|
64
|
+
| `OTP_EVENT_NAME` | `'onOtpReceived'` |
|
|
65
|
+
| `startOtpListener(options?)` | Starts SMS polling (READ_SMS); optional `length` (4–8, default 6). Requests permission if needed. |
|
|
66
|
+
| `stopOtpListener()` | Stops the READ_SMS listener. |
|
|
67
|
+
| `getAppHash()` | **Promise\<string | null\>** — Returns the 11-char app hash for SMS Retriever messages. |
|
|
68
|
+
| `startSmsRetrieverListener(options?)` | Starts SMS Retriever (no permission). Message must include app hash; ~5 min timeout. |
|
|
69
|
+
| `stopSmsRetrieverListener()` | Stops the SMS Retriever listener. |
|
|
70
|
+
| `addSmsRetrieverTimeoutListener(callback)` | Called when SMS Retriever times out (~5 min). Use for "Resend" / "Didn't get code?". |
|
|
71
|
+
| `addOtpListener(callback)` | Subscribes to OTP events; returns unsubscribe function. |
|
|
72
|
+
| `SMS_RETRIEVER_TIMEOUT_EVENT_NAME` | `'onSmsRetrieverTimeout'` — event name for timeout. |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Automatic SMS verification with the SMS Retriever API (no permission)
|
|
77
|
+
|
|
78
|
+
The **SMS Retriever API** lets you verify users via SMS **without READ_SMS**. The user does not grant any extra permissions; your server sends an SMS that contains a unique hash identifying your app, and the system delivers that message only to your app.
|
|
79
|
+
|
|
80
|
+
### 1. Get your app hash
|
|
81
|
+
|
|
82
|
+
Call `getAppHash()` and pass the value to your backend (e.g. at build time or via a one-time setup endpoint):
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { getAppHash } from 'expo-android-otp-autofill';
|
|
86
|
+
|
|
87
|
+
const hash = await getAppHash();
|
|
88
|
+
// e.g. "AbCdEfGhIjK" — give this to your server so it can include it in SMS
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 2. Message format / structure
|
|
92
|
+
|
|
93
|
+
The verification SMS **must**:
|
|
94
|
+
|
|
95
|
+
- Be **no longer than 140 bytes**
|
|
96
|
+
- Contain a **one-time code** (e.g. 4–8 digits)
|
|
97
|
+
- Include the **11-character hash** that identifies your app (from `getAppHash()`)
|
|
98
|
+
|
|
99
|
+
The exact format is flexible. Example:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Your verification code is 123456
|
|
103
|
+
|
|
104
|
+
AbCdEfGhIjK
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
(Replace `AbCdEfGhIjK` with your actual hash.)
|
|
108
|
+
|
|
109
|
+
For the full rules and how to compute the hash on the server, see the official guide:
|
|
110
|
+
**[SMS Retriever API – Construct a verification message](https://developers.google.com/identity/sms-retriever/verify#1_construct_a_verification_message)** (Google for Developers).
|
|
111
|
+
|
|
112
|
+
### 3. Use the hook or imperative API
|
|
113
|
+
|
|
114
|
+
**Easiest:** use `useOtpVerify` — it starts the retriever on mount and gives you `hash`, `otp`, `message`, `timeoutError`, `startListener`, `stopListener`:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const { hash, otp, timeoutError, startListener, stopListener } = useOtpVerify({
|
|
118
|
+
numberOfDigits: 6,
|
|
119
|
+
onOtpReceived: (otp) => setCode(otp),
|
|
120
|
+
});
|
|
121
|
+
// On timeout, show "Resend" and call startListener() to retry.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Or use the imperative API (`addOtpListener`, `addSmsRetrieverTimeoutListener`, `startSmsRetrieverListener`, `stopSmsRetrieverListener`) if you prefer.
|
|
125
|
+
|
|
126
|
+
The retriever waits for a matching SMS for about **5 minutes**, then sets `timeoutError`. Call `startListener()` to retry.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## READ_SMS mode (any SMS format)
|
|
131
|
+
|
|
132
|
+
If you prefer to use the **READ_SMS** permission, the module reads the **most recent SMS** from the **last 2 minutes** and matches an OTP of the length you set (default 6; 4–8 supported). No special message format required. Example formats:
|
|
133
|
+
|
|
134
|
+
- `991233 is your OTP for …`
|
|
135
|
+
- `Your code is 991233`
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Phone number (optional)
|
|
140
|
+
|
|
141
|
+
For **phone number retrieval** without the user typing it, use the **Phone Number Hint API** (Android). This module does not implement it; you can use it alongside this one. See: [Request a phone number](https://developers.google.com/identity/sms-retriever/request#request_a_phone_number) (Google for Developers).
|
|
142
|
+
|
|
143
|
+
## Supported versions
|
|
144
|
+
|
|
145
|
+
- **Expo**: 50+
|
|
146
|
+
- **React Native**: 0.72+
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id 'com.android.library'
|
|
3
|
+
id 'expo-module-gradle-plugin'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
group = 'expo.modules.androidotpautofill'
|
|
7
|
+
version = '0.1.0'
|
|
8
|
+
|
|
9
|
+
android {
|
|
10
|
+
namespace "expo.modules.androidotpautofill"
|
|
11
|
+
defaultConfig {
|
|
12
|
+
versionCode 1
|
|
13
|
+
versionName '0.1.0'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
dependencies {
|
|
18
|
+
implementation 'com.google.android.gms:play-services-auth:20.7.0'
|
|
19
|
+
implementation 'com.google.android.gms:play-services-auth-api-phone:18.0.2'
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package expo.modules.androidotpautofill
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import java.security.MessageDigest
|
|
7
|
+
import java.util.Arrays
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Computes the 11-character app hash string required for SMS Retriever API verification messages.
|
|
11
|
+
* Your server must include this hash in the SMS so the API can deliver the message to your app.
|
|
12
|
+
* See: https://developers.google.com/identity/sms-retriever/verify#1_construct_a_verification_message
|
|
13
|
+
*/
|
|
14
|
+
object AppSignatureHelper {
|
|
15
|
+
private const val HASH_LENGTH = 11
|
|
16
|
+
|
|
17
|
+
fun getAppHash(context: Context): String? {
|
|
18
|
+
val packageName = context.packageName
|
|
19
|
+
val signature = getSigningCertificateHex(context, packageName) ?: return null
|
|
20
|
+
val appInfo = "$packageName $signature"
|
|
21
|
+
val digest = MessageDigest.getInstance("SHA-256").apply {
|
|
22
|
+
update(appInfo.toByteArray(Charsets.UTF_8))
|
|
23
|
+
}.digest()
|
|
24
|
+
val hashBytes = Arrays.copyOfRange(digest, 0, 9)
|
|
25
|
+
val base64 = android.util.Base64.encodeToString(hashBytes, android.util.Base64.NO_PADDING or android.util.Base64.NO_WRAP)
|
|
26
|
+
return base64.take(HASH_LENGTH)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private fun getSigningCertificateHex(context: Context, packageName: String): String? {
|
|
30
|
+
return try {
|
|
31
|
+
val pm = context.packageManager
|
|
32
|
+
@Suppress("DEPRECATION")
|
|
33
|
+
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
34
|
+
PackageManager.GET_SIGNING_CERTIFICATES
|
|
35
|
+
} else {
|
|
36
|
+
PackageManager.GET_SIGNATURES
|
|
37
|
+
}
|
|
38
|
+
val packageInfo = pm.getPackageInfo(packageName, flags)
|
|
39
|
+
val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
40
|
+
packageInfo.signingInfo?.apkContentsSigners
|
|
41
|
+
} else {
|
|
42
|
+
@Suppress("DEPRECATION")
|
|
43
|
+
packageInfo.signatures
|
|
44
|
+
}
|
|
45
|
+
val sig = signatures?.firstOrNull() ?: return null
|
|
46
|
+
sig.toByteArray().joinToString("") { "%02x".format(it) }
|
|
47
|
+
} catch (e: Exception) {
|
|
48
|
+
null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
package expo.modules.androidotpautofill
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.Manifest
|
|
5
|
+
import android.content.BroadcastReceiver
|
|
6
|
+
import android.content.ContentResolver
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.content.IntentFilter
|
|
10
|
+
import android.content.pm.PackageManager
|
|
11
|
+
import android.net.Uri
|
|
12
|
+
import android.os.Build
|
|
13
|
+
import android.os.Handler
|
|
14
|
+
import android.os.Looper
|
|
15
|
+
import android.provider.Telephony
|
|
16
|
+
import androidx.core.app.ActivityCompat
|
|
17
|
+
import com.google.android.gms.auth.api.phone.SmsRetriever
|
|
18
|
+
import com.google.android.gms.common.api.CommonStatusCodes
|
|
19
|
+
import expo.modules.kotlin.exception.Exceptions
|
|
20
|
+
import expo.modules.kotlin.modules.Module
|
|
21
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
22
|
+
import java.util.regex.Pattern
|
|
23
|
+
|
|
24
|
+
class ExpoAndroidOtpAutofillModule : Module() {
|
|
25
|
+
private val context: android.content.Context
|
|
26
|
+
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
|
27
|
+
|
|
28
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
29
|
+
private var pollRunnable: Runnable? = null
|
|
30
|
+
private var lastCheckedMessageId: Long = -1
|
|
31
|
+
private var otpLength: Int = 6
|
|
32
|
+
|
|
33
|
+
private var smsRetrieverReceiver: BroadcastReceiver? = null
|
|
34
|
+
|
|
35
|
+
override fun definition() = ModuleDefinition {
|
|
36
|
+
Name("ExpoAndroidOtpAutofill")
|
|
37
|
+
|
|
38
|
+
Events("onOtpReceived", "onSmsRetrieverTimeout")
|
|
39
|
+
|
|
40
|
+
AsyncFunction("getAppHash") {
|
|
41
|
+
AppSignatureHelper.getAppHash(context)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
AsyncFunction("startOtpListener") { length: Int? ->
|
|
45
|
+
if (length != null && length in 4..8) {
|
|
46
|
+
otpLength = length
|
|
47
|
+
}
|
|
48
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
49
|
+
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) != PackageManager.PERMISSION_GRANTED) {
|
|
50
|
+
appContext.currentActivity?.let { activity: Activity ->
|
|
51
|
+
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_SMS), 1001)
|
|
52
|
+
}
|
|
53
|
+
handler.postDelayed({ startPolling() }, 2000)
|
|
54
|
+
return@AsyncFunction
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
startPolling()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
AsyncFunction<Unit>("removeListener") {
|
|
61
|
+
stopPolling()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
AsyncFunction("startSmsRetrieverListener") { length: Int? ->
|
|
65
|
+
if (length != null && length in 4..8) {
|
|
66
|
+
otpLength = length
|
|
67
|
+
}
|
|
68
|
+
startSmsRetriever()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
AsyncFunction<Unit>("removeSmsRetrieverListener") {
|
|
72
|
+
unregisterSmsRetrieverReceiver()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun startPolling() {
|
|
77
|
+
if (pollRunnable != null) return
|
|
78
|
+
pollRunnable = object : Runnable {
|
|
79
|
+
override fun run() {
|
|
80
|
+
val otp = readOtpFromLastSms()
|
|
81
|
+
if (otp != null) {
|
|
82
|
+
stopPolling()
|
|
83
|
+
sendEvent("onOtpReceived", mapOf("otp" to otp))
|
|
84
|
+
} else {
|
|
85
|
+
handler.postDelayed(this, 2500)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
handler.post(pollRunnable!!)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private fun stopPolling() {
|
|
93
|
+
pollRunnable?.let { handler.removeCallbacks(it) }
|
|
94
|
+
pollRunnable = null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private fun readOtpFromLastSms(): String? {
|
|
98
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
|
99
|
+
ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) != PackageManager.PERMISSION_GRANTED
|
|
100
|
+
) {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
val cr: ContentResolver = context.contentResolver
|
|
104
|
+
val uri: Uri = Telephony.Sms.CONTENT_URI
|
|
105
|
+
val cursor = cr.query(
|
|
106
|
+
uri,
|
|
107
|
+
arrayOf(Telephony.Sms._ID, Telephony.Sms.BODY, Telephony.Sms.DATE),
|
|
108
|
+
null,
|
|
109
|
+
null,
|
|
110
|
+
Telephony.Sms.DEFAULT_SORT_ORDER
|
|
111
|
+
) ?: return null
|
|
112
|
+
try {
|
|
113
|
+
if (cursor.moveToFirst()) {
|
|
114
|
+
val id = cursor.getLong(cursor.getColumnIndexOrThrow(Telephony.Sms._ID))
|
|
115
|
+
if (id == lastCheckedMessageId) return null
|
|
116
|
+
lastCheckedMessageId = id
|
|
117
|
+
val body = cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Sms.BODY)) ?: return null
|
|
118
|
+
val date = cursor.getLong(cursor.getColumnIndexOrThrow(Telephony.Sms.DATE))
|
|
119
|
+
val twoMinutesAgo = System.currentTimeMillis() - 2 * 60 * 1000
|
|
120
|
+
if (date < twoMinutesAgo) return null
|
|
121
|
+
val pattern = Pattern.compile("\\d{$otpLength}")
|
|
122
|
+
val matcher = pattern.matcher(body)
|
|
123
|
+
if (matcher.find()) return matcher.group()
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
cursor.close()
|
|
127
|
+
}
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fun startSmsRetriever() {
|
|
132
|
+
unregisterSmsRetrieverReceiver()
|
|
133
|
+
val client = SmsRetriever.getClient(context)
|
|
134
|
+
client.startSmsRetriever().addOnSuccessListener {
|
|
135
|
+
val receiver = object : BroadcastReceiver() {
|
|
136
|
+
override fun onReceive(ctx: Context?, intent: Intent?) {
|
|
137
|
+
if (intent?.action != SmsRetriever.SMS_RETRIEVED_ACTION) return
|
|
138
|
+
val extras = intent.extras ?: return
|
|
139
|
+
@Suppress("DEPRECATION")
|
|
140
|
+
val status = extras.get(SmsRetriever.EXTRA_STATUS) as? com.google.android.gms.common.api.Status ?: return
|
|
141
|
+
when (status.statusCode) {
|
|
142
|
+
CommonStatusCodes.SUCCESS -> {
|
|
143
|
+
val message = extras.getString(SmsRetriever.EXTRA_SMS_MESSAGE) ?: return
|
|
144
|
+
val pattern = Pattern.compile("\\d{$otpLength}")
|
|
145
|
+
val matcher = pattern.matcher(message)
|
|
146
|
+
if (matcher.find()) {
|
|
147
|
+
unregisterSmsRetrieverReceiver()
|
|
148
|
+
sendEvent("onOtpReceived", mapOf("otp" to matcher.group()!!, "message" to message))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
CommonStatusCodes.TIMEOUT -> {
|
|
152
|
+
sendEvent("onSmsRetrieverTimeout", mapOf("timedOut" to true))
|
|
153
|
+
unregisterSmsRetrieverReceiver()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
smsRetrieverReceiver = receiver
|
|
159
|
+
val filter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
|
|
160
|
+
val appContext = context.applicationContext
|
|
161
|
+
if (Build.VERSION.SDK_INT >= 33) {
|
|
162
|
+
appContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
|
163
|
+
} else {
|
|
164
|
+
@Suppress("DEPRECATION")
|
|
165
|
+
appContext.registerReceiver(receiver, filter)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private fun unregisterSmsRetrieverReceiver() {
|
|
171
|
+
val receiver = smsRetrieverReceiver ?: return
|
|
172
|
+
try {
|
|
173
|
+
context.applicationContext.unregisterReceiver(receiver)
|
|
174
|
+
} catch (_: Exception) {}
|
|
175
|
+
smsRetrieverReceiver = null
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package expo.modules.androidotpautofill
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.webkit.WebView
|
|
5
|
+
import android.webkit.WebViewClient
|
|
6
|
+
import expo.modules.kotlin.AppContext
|
|
7
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
8
|
+
import expo.modules.kotlin.views.ExpoView
|
|
9
|
+
|
|
10
|
+
class ExpoAndroidOtpAutofillView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
11
|
+
// Creates and initializes an event dispatcher for the `onLoad` event.
|
|
12
|
+
// The name of the event is inferred from the value and needs to match the event name defined in the module.
|
|
13
|
+
private val onLoad by EventDispatcher()
|
|
14
|
+
|
|
15
|
+
// Defines a WebView that will be used as the root subview.
|
|
16
|
+
internal val webView = WebView(context).apply {
|
|
17
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
18
|
+
webViewClient = object : WebViewClient() {
|
|
19
|
+
override fun onPageFinished(view: WebView, url: String) {
|
|
20
|
+
// Sends an event to JavaScript. Triggers a callback defined on the view component in JavaScript.
|
|
21
|
+
onLoad(mapOf("url" to url))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
init {
|
|
27
|
+
// Adds the WebView to the view hierarchy.
|
|
28
|
+
addView(webView)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAndroidOtpAutofill.d.ts","sourceRoot":"","sources":["../src/ExpoAndroidOtpAutofill.ts"],"names":[],"mappings":";AAEA,wBAAqE"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
|
+
export type OnLoadEventPayload = {
|
|
3
|
+
url: string;
|
|
4
|
+
};
|
|
5
|
+
export type OnOtpReceivedPayload = {
|
|
6
|
+
otp: string;
|
|
7
|
+
/** Full SMS body when received via SMS Retriever API; not set for READ_SMS. */
|
|
8
|
+
message?: string;
|
|
9
|
+
};
|
|
10
|
+
export type OnSmsRetrieverTimeoutPayload = {
|
|
11
|
+
timedOut: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type ExpoAndroidOtpAutofillModuleEvents = {
|
|
14
|
+
onOtpReceived: (payload: OnOtpReceivedPayload) => void;
|
|
15
|
+
onSmsRetrieverTimeout: (payload: OnSmsRetrieverTimeoutPayload) => void;
|
|
16
|
+
};
|
|
17
|
+
export type ChangeEventPayload = {
|
|
18
|
+
value: string;
|
|
19
|
+
};
|
|
20
|
+
export type ExpoAndroidOtpAutofillViewProps = {
|
|
21
|
+
url: string;
|
|
22
|
+
onLoad: (event: {
|
|
23
|
+
nativeEvent: OnLoadEventPayload;
|
|
24
|
+
}) => void;
|
|
25
|
+
style?: StyleProp<ViewStyle>;
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=ExpoAndroidOtpAutofill.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAndroidOtpAutofill.types.d.ts","sourceRoot":"","sources":["../src/ExpoAndroidOtpAutofill.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,kCAAkC,GAAG;IAC/C,aAAa,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACvD,qBAAqB,EAAE,CAAC,OAAO,EAAE,4BAA4B,KAAK,IAAI,CAAC;CACxE,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,kBAAkB,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7D,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ExpoAndroidOtpAutofillViewProps } from './ExpoAndroidOtpAutofill.types';
|
|
3
|
+
export default function ExpoAndroidOtpAutofillView(props: ExpoAndroidOtpAutofillViewProps): React.JSX.Element;
|
|
4
|
+
//# sourceMappingURL=ExpoAndroidOtpAutofillView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAndroidOtpAutofillView.d.ts","sourceRoot":"","sources":["../src/ExpoAndroidOtpAutofillView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,+BAA+B,EAAE,MAAM,gCAAgC,CAAC;AAKjF,MAAM,CAAC,OAAO,UAAU,0BAA0B,CAAC,KAAK,EAAE,+BAA+B,qBAExF"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event name emitted by the native module when an OTP is read from SMS.
|
|
3
|
+
* Subscribe with `addOtpListener()`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const OTP_EVENT_NAME = "onOtpReceived";
|
|
6
|
+
/**
|
|
7
|
+
* Event name emitted when the SMS Retriever API times out (~5 minutes without a matching SMS).
|
|
8
|
+
* Subscribe with `addSmsRetrieverTimeoutListener()`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SMS_RETRIEVER_TIMEOUT_EVENT_NAME = "onSmsRetrieverTimeout";
|
|
11
|
+
/** True when running on Android (OTP from SMS is only supported on Android). */
|
|
12
|
+
export declare function isAndroid(): boolean;
|
|
13
|
+
/** Default OTP length used when not specified (common for SMS codes). */
|
|
14
|
+
export declare const DEFAULT_OTP_LENGTH = 6;
|
|
15
|
+
/** Supported OTP length (4–8 digits). Used to match OTP in SMS body. */
|
|
16
|
+
export type OtpLength = 4 | 5 | 6 | 7 | 8;
|
|
17
|
+
export type StartOtpListenerOptions = {
|
|
18
|
+
/** Number of digits to match (4–8). Default 6. */
|
|
19
|
+
length?: OtpLength;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Start listening for incoming SMS and parsing OTP of the given length.
|
|
23
|
+
* Requests READ_SMS permission if needed. Call `stopOtpListener()` when done.
|
|
24
|
+
* No-op on iOS and web.
|
|
25
|
+
* @param options Optional `{ length: 4|5|6|7|8 }`. Default 6 when omitted.
|
|
26
|
+
*/
|
|
27
|
+
export declare function startOtpListener(options?: StartOtpListenerOptions): void;
|
|
28
|
+
/**
|
|
29
|
+
* Stop the SMS listener and polling.
|
|
30
|
+
* No-op on iOS and web.
|
|
31
|
+
*/
|
|
32
|
+
export declare function stopOtpListener(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Get the 11-character app hash required for SMS Retriever API verification messages.
|
|
35
|
+
* Your backend must include this hash in the SMS so the message can be delivered to your app.
|
|
36
|
+
* No permission required. Returns null on iOS/web or if unavailable.
|
|
37
|
+
* @see https://developers.google.com/identity/sms-retriever/verify
|
|
38
|
+
*/
|
|
39
|
+
export declare function getAppHash(): Promise<string | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Start listening via SMS Retriever API (no READ_SMS permission).
|
|
42
|
+
* The SMS must contain your app hash (from getAppHash()) and be ≤140 bytes.
|
|
43
|
+
* Times out after ~5 minutes. Same onOtpReceived events as startOtpListener.
|
|
44
|
+
* No-op on iOS and web.
|
|
45
|
+
*/
|
|
46
|
+
export declare function startSmsRetrieverListener(options?: StartOtpListenerOptions): void;
|
|
47
|
+
/**
|
|
48
|
+
* Stop the SMS Retriever listener (unregister receiver).
|
|
49
|
+
* No-op on iOS and web.
|
|
50
|
+
*/
|
|
51
|
+
export declare function stopSmsRetrieverListener(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Subscribe to OTP events. Call the returned function to unsubscribe.
|
|
54
|
+
* @param callback Called with the OTP string (4–8 digits) when detected from SMS.
|
|
55
|
+
* @returns Unsubscribe function.
|
|
56
|
+
*/
|
|
57
|
+
export declare function addOtpListener(callback: (otp: string) => void): () => void;
|
|
58
|
+
/**
|
|
59
|
+
* Subscribe to SMS Retriever timeout events (fired when no matching SMS is received within ~5 minutes).
|
|
60
|
+
* Use this to show "Didn't get the code?" / "Resend" when using startSmsRetrieverListener().
|
|
61
|
+
* @param callback Called when the retriever times out.
|
|
62
|
+
* @returns Unsubscribe function.
|
|
63
|
+
*/
|
|
64
|
+
export declare function addSmsRetrieverTimeoutListener(callback: () => void): () => void;
|
|
65
|
+
//# sourceMappingURL=OtpAutofill.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OtpAutofill.d.ts","sourceRoot":"","sources":["../src/OtpAutofill.ts"],"names":[],"mappings":"AAKA;;;GAGG;AACH,eAAO,MAAM,cAAc,kBAAkB,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,gCAAgC,0BAA0B,CAAC;AAExE,gFAAgF;AAChF,wBAAgB,SAAS,IAAI,OAAO,CAEnC;AAED,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,wEAAwE;AACxE,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAE1C,MAAM,MAAM,uBAAuB,GAAG;IACpC,kDAAkD;IAClD,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,IAAI,CAQxE;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAItC;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGzD;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,IAAI,CAQjF;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAI/C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAK1E;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAK/E"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { startOtpListener, stopOtpListener, addOtpListener, getAppHash, startSmsRetrieverListener, stopSmsRetrieverListener, addSmsRetrieverTimeoutListener, OTP_EVENT_NAME, SMS_RETRIEVER_TIMEOUT_EVENT_NAME, DEFAULT_OTP_LENGTH, } from './OtpAutofill';
|
|
2
|
+
export type { OtpLength, StartOtpListenerOptions } from './OtpAutofill';
|
|
3
|
+
export { useOtpVerify } from './useOtpVerify';
|
|
4
|
+
export type { UseOtpVerifyOptions, UseOtpVerifyResult } from './useOtpVerify';
|
|
5
|
+
export { default } from './ExpoAndroidOtpAutofill';
|
|
6
|
+
export { default as ExpoAndroidOtpAutofillView } from './ExpoAndroidOtpAutofillView';
|
|
7
|
+
export type { ExpoAndroidOtpAutofillModuleEvents, OnOtpReceivedPayload, OnSmsRetrieverTimeoutPayload } from './ExpoAndroidOtpAutofill.types';
|
|
8
|
+
export * from './ExpoAndroidOtpAutofill.types';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,UAAU,EACV,yBAAyB,EACzB,wBAAwB,EACxB,8BAA8B,EAC9B,cAAc,EACd,gCAAgC,EAChC,kBAAkB,GACnB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,SAAS,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,OAAO,IAAI,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AACrF,YAAY,EAAE,kCAAkC,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC7I,cAAc,gCAAgC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { OtpLength } from './OtpAutofill';
|
|
2
|
+
export type UseOtpVerifyOptions = {
|
|
3
|
+
/** Number of OTP digits to match (4–8). Default 6. */
|
|
4
|
+
numberOfDigits?: OtpLength;
|
|
5
|
+
/** Called when an OTP is received (e.g. to sync to a form field). */
|
|
6
|
+
onOtpReceived?: (otp: string) => void;
|
|
7
|
+
};
|
|
8
|
+
export type UseOtpVerifyResult = {
|
|
9
|
+
/** App hash for SMS Retriever (from getAppHash()). Include in your SMS template. */
|
|
10
|
+
hash: string | null;
|
|
11
|
+
/** OTP extracted from the last received SMS, or null. */
|
|
12
|
+
otp: string | null;
|
|
13
|
+
/** Full SMS message when received via SMS Retriever; null otherwise. */
|
|
14
|
+
message: string | null;
|
|
15
|
+
/** True when SMS Retriever timed out (~5 min). Show "Resend" or "Didn't get code?". */
|
|
16
|
+
timeoutError: boolean;
|
|
17
|
+
/** Start listening again (e.g. after timeout or to retry). */
|
|
18
|
+
startListener: () => void;
|
|
19
|
+
/** Stop the SMS Retriever listener. */
|
|
20
|
+
stopListener: () => void;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Hook for SMS Retriever API (no READ_SMS).
|
|
24
|
+
* Returns hash, otp, message, timeoutError, startListener, stopListener.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const { hash, otp, message, timeoutError, startListener, stopListener } = useOtpVerify({ numberOfDigits: 4 });
|
|
28
|
+
*/
|
|
29
|
+
export declare function useOtpVerify(options?: UseOtpVerifyOptions): UseOtpVerifyResult;
|
|
30
|
+
//# sourceMappingURL=useOtpVerify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useOtpVerify.d.ts","sourceRoot":"","sources":["../src/useOtpVerify.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,MAAM,mBAAmB,GAAG;IAChC,sDAAsD;IACtD,cAAc,CAAC,EAAE,SAAS,CAAC;IAC3B,qEAAqE;IACrE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,oFAAoF;IACpF,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,yDAAyD;IACzD,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,wEAAwE;IACxE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,uFAAuF;IACvF,YAAY,EAAE,OAAO,CAAC;IACtB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,uCAAuC;IACvC,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAIF;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,kBAAkB,CAwDlF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "expo-android-otp-autofill",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Android-only Expo module to auto-detect OTP from SMS and fill your verification field. READ_SMS and Play Services Auth are declared in the module. No config plugin or third-party OTP library required.",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "expo-module build",
|
|
9
|
+
"clean": "expo-module clean",
|
|
10
|
+
"lint": "expo-module lint",
|
|
11
|
+
"test": "expo-module test",
|
|
12
|
+
"prepare": "expo-module prepare",
|
|
13
|
+
"prepublishOnly": "expo-module prepublishOnly",
|
|
14
|
+
"expo-module": "expo-module",
|
|
15
|
+
"open:android": "open -a \"Android Studio\" example/android"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"react-native",
|
|
19
|
+
"expo",
|
|
20
|
+
"android",
|
|
21
|
+
"otp",
|
|
22
|
+
"sms",
|
|
23
|
+
"autofill",
|
|
24
|
+
"expo-android-otp-autofill",
|
|
25
|
+
"ExpoAndroidOtpAutofill"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/mayurbabariya45/expo-android-otp-autofill.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/mayurbabariya45/expo-android-otp-autofill/issues"
|
|
33
|
+
},
|
|
34
|
+
"author": "Mayur Babaria <mayurbabariya45@gmail.com> (https://github.com/mayurbabariya45)",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"homepage": "https://github.com/mayurbabariya45/expo-android-otp-autofill#readme",
|
|
37
|
+
"files": [
|
|
38
|
+
"build",
|
|
39
|
+
"android",
|
|
40
|
+
"expo-module.config.json"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/react": "~19.1.1",
|
|
45
|
+
"expo-module-scripts": "^55.0.2",
|
|
46
|
+
"expo": "^55.0.5",
|
|
47
|
+
"react-native": "0.82.1"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"expo": "*",
|
|
51
|
+
"react": "*",
|
|
52
|
+
"react-native": "*"
|
|
53
|
+
}
|
|
54
|
+
}
|