How to Implement SMS Retrieval without User SMS Permission in React Native
In this guide, I'll walk you through the process of implementing SMS retrieval in React Native using the SMS User Consent API. This method allows your app to automatically read a one-time password (OTP) from an SMS, with the user's consent, without requiring any special permissions or modifications in the AndroidManifest.xml file.
Why Use SMS User Consent API?
The SMS User Consent API is a secure way to read a single SMS for OTP verification. It only works with a specific message that contains a one-time code, ensuring that your app only reads the message relevant to the verification process.
Step-by-Step Implementation
1. Install Dependencies
First, include the necessary dependencies in your build.gradle file:
dependencies {
implementation 'com.google.android.gms:play-services-auth:17.0.0'
implementation 'com.google.android.gms:play-services-auth-api-phone:17.4.0'
}
This ensures that your app can use the SMS User Consent API from Google Play Services.
2. Create a Native Module for SMS Retrieval
You need to create a native module in Android to handle the SMS retrieval process. This will bridge the native code with your React Native app.
SmsRetrieverModule.java
package com.yourappname;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.google.android.gms.auth.api.phone.SmsRetriever;
import com.google.android.gms.auth.api.phone.SmsRetrieverClient;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.api.Status;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SmsRetrieverModule extends ReactContextBaseJavaModule {
private static final int SMS_CONSENT_REQUEST = 200;
private final BroadcastReceiver smsVerificationReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (SmsRetriever.SMS_RETRIEVED_ACTION.equals(intent.getAction())) {
Bundle extras = intent.getExtras();
Status smsRetrieverStatus = (Status) extras.get(SmsRetriever.EXTRA_STATUS);
switch (smsRetrieverStatus.getStatusCode()) {
case CommonStatusCodes.SUCCESS:
Intent consentIntent = extras.getParcelable(SmsRetriever.EXTRA_CONSENT_INTENT);
try {
getCurrentActivity().startActivityForResult(consentIntent, SMS_CONSENT_REQUEST);
} catch (ActivityNotFoundException e) {
// Handle the exception
}
break;
case CommonStatusCodes.TIMEOUT:
// Handle timeout
break;
}
}
}
};
public SmsRetrieverModule(ReactApplicationContext reactContext) {
super(reactContext);
IntentFilter intentFilter = new IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION);
reactContext.registerReceiver(smsVerificationReceiver, intentFilter);
}
@NonNull
@Override
public String getName() {
return "SmsRetrieverModule";
}
@ReactMethod
public void startSmsUserConsent() {
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
SmsRetrieverClient client = SmsRetriever.getClient(currentActivity);
client.startSmsUserConsent(null)
.addOnSuccessListener(task -> {
Log.d("SmsRetriever", "Successfully started SMS User Consent");
})
.addOnFailureListener(e -> {
Log.e("SmsRetriever", "Failed to start SMS User Consent", e);
sendEvent("onSmsReceivedError", "Failed to start SMS User Consent");
});
} else {
Log.e("SmsRetriever", "Current activity is null");
sendEvent("onSmsReceivedError", "Current activity is null");
}
}
private void sendEvent(String eventName, String data) {
getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, data);
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == SMS_CONSENT_REQUEST && resultCode == Activity.RESULT_OK) {
String message = data.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE);
String oneTimeCode = parseOneTimeCode(message);
sendEvent("onSmsReceived", oneTimeCode);
} else {
sendEvent("onSmsReceivedError", "Consent canceled or failed");
}
}
private String parseOneTimeCode(String message) {
Pattern otpPattern = Pattern.compile("\\b\\d{6}\\b");
Matcher matcher = otpPattern.matcher(message);
if (matcher.find()) {
return matcher.group(0);
}
return "";
}
}
MainActivity.java
package com.yourappname;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;
public class MainActivity extends ReactActivity {
private SmsRetrieverModule smsRetrieverModule;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected String getMainComponentName() {
return "YourAppName";
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new DefaultReactActivityDelegate(
this,
getMainComponentName(),
DefaultNewArchitectureEntryPoint.getFabricEnabled());
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
smsRetrieverModule.onActivityResult(requestCode, resultCode, data);
}
}
3. Handling the OTP in React Native
To handle the OTP on the React Native side, you need to call the native module and listen for events.
import { NativeModules, NativeEventEmitter } from 'react-native';
const { SmsRetrieverModule } = NativeModules;
export const startSmsRetriever = () => {
SmsRetrieverModule.startSmsUserConsent();
};
const eventEmitter = new NativeEventEmitter(SmsRetrieverModule);
eventEmitter.addListener('onSmsReceived', (otp) => {
console.log('Received OTP: ', otp);
// Handle OTP
});
eventEmitter.addListener('onSmsReceivedError', (error) => {
console.error('Failed to receive OTP: ', error);
});
Conclusion
This implementation provides a secure and user-friendly way to retrieve OTPs for verification in your React Native app. It enhances the user experience by automatically detecting and reading OTPs without requiring additional permissions, and only for the relevant message.
Feel free to share this implementation with your network!