How to Implement SMS Retrieval without User SMS Permission in React Native

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.


Article content

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!

To view or add a comment, sign in

More articles by Manav Garg

Explore content categories