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 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,4 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!-- Required for reading SMS to auto-detect OTP -->
3
+ <uses-permission android:name="android.permission.READ_SMS" />
4
+ </manifest>
@@ -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,3 @@
1
+ declare const _default: any;
2
+ export default _default;
3
+ //# sourceMappingURL=ExpoAndroidOtpAutofill.d.ts.map
@@ -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"}
@@ -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"}
@@ -0,0 +1,6 @@
1
+ {
2
+ "platforms": ["android"],
3
+ "android": {
4
+ "modules": ["expo.modules.androidotpautofill.ExpoAndroidOtpAutofillModule"]
5
+ }
6
+ }
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
+ }