11

In my app, I'm using Firebase to verify the user's phone number. However, the verification system is inconsistent and sends the OTP only the first time. For example, I get the OTP when I'm signing in for the first time, but if I sign out and try to sign in again, I don't get the OTP.

This is the activity where the user is asked to enter the OTP:

import android.app.ProgressDialog;
import android.content.Intent;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.firebase.FirebaseException;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.PhoneAuthCredential;
import com.google.firebase.auth.PhoneAuthProvider;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.QueryDocumentSnapshot;
import com.google.firebase.firestore.QuerySnapshot;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;

import java.util.concurrent.TimeUnit;

public class EnterOTPActivity extends AppCompatActivity {
    String userPhoneNumber;

    private int autoResendCount=0, resumeCount=0;
    private final String ctryCode = "+91";
    private String verificationId;
    private FirebaseAuth mAuth;
    private TextView resendOtp, enterOtpMessage, timerTV;
    private ProgressDialog progressDialog;
    private EditText otpField;
    private boolean firstSend=true,timerOn=true;
    FirebaseFirestore db;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_enter_otp);
        mAuth = FirebaseAuth.getInstance();
        otpField = findViewById(R.id.editTextOtp);
        resendOtp =  findViewById(R.id.resendOTPButton);
        timerTV = findViewById(R.id.resendTimer);
        userPhoneNumber = getIntent().getStringExtra("USER_MOB").trim();
        sendVerificationCode(userPhoneNumber);
        startResendTimer(15);
        getSupportActionBar().setTitle("Verification");
        resendOtp.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (!timerOn) {
                    startResendTimer(30);
                    firstSend = false;
                    sendVerificationCode(userPhoneNumber);
                }
            }
        });
        FloatingActionButton next = findViewById(R.id.fab2);
        enterOtpMessage =  findViewById(R.id.aboutotpverif);
        enterOtpMessage.setText(enterOtpMessage.getText().toString() + " " + ctryCode + userPhoneNumber);
        progressDialog = new ProgressDialog(this);
        progressDialog.setMessage("Please wait while we check your verification code");
        progressDialog.setTitle("Verification");
        progressDialog.setCancelable(false);
        next.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //check if otp is correct here
                if (otpField.getText().toString().length() == 6) {
                    verifyCode(otpField.getText().toString().trim());
                    if(!progressDialog.isShowing())
                    progressDialog.show();
                } else {
                    View parentLayout = findViewById(android.R.id.content);
                    Snackbar.make(parentLayout, "A valid verification code has 6 digits", Snackbar.LENGTH_SHORT)
                            .setAction("OKAY", new View.OnClickListener() {
                                @Override
                                public void onClick(View view) {

                                }
                            })
                            .setActionTextColor(getResources().getColor(android.R.color.holo_red_light))
                            .show();
                }
            }
        });
    }

    private void verifyCode(String code) {

        try {
            PhoneAuthCredential credential = PhoneAuthProvider.getCredential(verificationId, code);
            signInwithCredential(credential);

        }
        catch (Exception e) {
            // code here is executed when we don't get the OTP and enter a wrong one
            progressDialog.dismiss();
            progressDialog.setCancelable(true);
            View parentLayout = findViewById(android.R.id.content);
            Snackbar.make(parentLayout, getString(R.string.incorrect_code_t1), Snackbar.LENGTH_LONG)
                    .setAction("OKAY", new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {

                        }
                    })
                    .setActionTextColor(getResources().getColor(android.R.color.holo_red_light))
                    .show();
            if(progressDialog.isShowing())
            progressDialog.dismiss();
        }
    }

    private void signInwithCredential(PhoneAuthCredential credential) {
        mAuth.signInWithCredential(credential).addOnCompleteListener(new OnCompleteListener<AuthResult>() {
            @Override
            public void onComplete(@NonNull Task<AuthResult> task) {
                if (task.isSuccessful()) {
                    isRegisteredUser();
                }
                else {
                    if(progressDialog.isShowing())
                    progressDialog.dismiss();
                    View parentLayout = findViewById(android.R.id.content);
                    Snackbar.make(parentLayout, "Incorrect verification code", Snackbar.LENGTH_SHORT)
                            .setAction("OKAY", new View.OnClickListener() {
                                @Override
                                public void onClick(View view) {

                                }
                            })
                            .setActionTextColor(getResources().getColor(android.R.color.holo_red_light))
                            .show();
                    if(progressDialog.isShowing())
                    progressDialog.dismiss();

                }
            }
        });

    }

    private void sendVerificationCode(String number) {
        final String phoneNumber = ctryCode + number;
        PhoneAuthProvider.getInstance().verifyPhoneNumber(
                phoneNumber,        // Phone number to verify
                120,                 // Timeout duration
                TimeUnit.SECONDS,   // Unit of timeout
                this,               // Activity (for callback binding)
                mCallbacks);        // OnVerificationStateChangedCallbacks
    }

    private PhoneAuthProvider.OnVerificationStateChangedCallbacks mCallbacks = new PhoneAuthProvider.OnVerificationStateChangedCallbacks() {

        @Override
        public void onCodeSent(@NonNull String s, @NonNull PhoneAuthProvider.ForceResendingToken forceResendingToken) {
            super.onCodeSent(s, forceResendingToken);
            verificationId = s;
        }

        @Override
        public void onVerificationCompleted(@NonNull PhoneAuthCredential phoneAuthCredential) {
            String code = phoneAuthCredential.getSmsCode();
            if (code != null) {
                verifyCode(code);
                otpField.setText(code);
                if(!progressDialog.isShowing())
                progressDialog.show();
            }
        }

        @Override
        public void onVerificationFailed(@NonNull FirebaseException e) {
            Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show();
        }
    };

    private void isRegisteredUser() {

        final String userId = ctryCode + userPhoneNumber;
        db = FirebaseFirestore.getInstance();
        db.collection("users")
                .whereEqualTo("phone", userId)
                .get()
                .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {

                    @Override
                    public void onComplete(@NonNull Task<QuerySnapshot> task) {
                        try{
                        if (task.isSuccessful()) {

                            saveToken(userId);

                            for (QueryDocumentSnapshot document : task.getResult()) {
                                if (document.getId().equalsIgnoreCase(userId)) {

                                    Intent startmainact = new Intent(EnterOTPActivity.this, MainActivity.class);
                                    startmainact.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
                                    startActivity(startmainact);
                                    finish();
                                    return;
                                }
                                //Log.d(TAG, document.getId() + " => " + document.getData());
                            }

                            Intent startposact = new Intent(EnterOTPActivity.this, ParOrStudActivity.class);

                            startposact.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
                            startActivity(startposact);
                            finish();

                        } else {
                            View parentLayout = findViewById(android.R.id.content);
                            Snackbar.make(parentLayout, "Database error: please try again", Snackbar.LENGTH_SHORT)
                                    .setAction("OKAY", new View.OnClickListener() {
                                        @Override
                                        public void onClick(View view) {

                                        }
                                    })
                                    .setActionTextColor(getResources().getColor(android.R.color.holo_red_light))
                                    .show();
                        }
                    }
                        catch(Exception e){
                            View parentLayout = findViewById(android.R.id.content);
                            Snackbar.make(parentLayout, "Database error: please try again", Snackbar.LENGTH_SHORT)
                                    .setAction("OKAY", new View.OnClickListener() {
                                        @Override
                                        public void onClick(View view) {

                                        }
                                    })
                                    .setActionTextColor(getResources().getColor(android.R.color.holo_red_light))
                                    .show();
                        }
                    }

                });
    }

    private void saveToken(final String userId) {
        FirebaseInstanceId.getInstance().getInstanceId()
                .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
                    @Override
                    public void onComplete(@NonNull Task<InstanceIdResult> task) {
                        if (!task.isSuccessful()) {
                            return;
                        }

                        // Get new Instance ID token
                        String token = task.getResult().getToken();
                        FirebaseFirestore.getInstance().collection("users").document(userId).update("token",token);
                    }
                });
    }


    @Override
    protected void onResume() {
        super.onResume();
        if (autoResendCount<2 && resumeCount>0) {
            sendVerificationCode(userPhoneNumber);
            autoResendCount=autoResendCount+1;
            resumeCount=resumeCount+1;
        }
    }


    @Override
    protected void onStop() {
        super.onStop();
        progressDialog.dismiss();
    }

    public void startResendTimer(int seconds) {
        timerTV.setVisibility(View.VISIBLE);
        resendOtp.setEnabled(false);

        new CountDownTimer(seconds*1000, 1000) {

            public void onTick(long millisUntilFinished) {
                String secondsString = Long.toString(millisUntilFinished/1000);
                if (millisUntilFinished<10000) {
                    secondsString = "0"+secondsString;
                }
                timerTV.setText(" (0:"+ secondsString+")");
            }

            public void onFinish() {
                resendOtp.setEnabled(true);
                timerTV.setVisibility(View.GONE);
                timerOn=false;
            }
        }.start();
    }

    @Override
    public void onBackPressed() {
        // do not allow user to go back to the screen before
    }
}

This erratic verification system delivers an incompetent UX. Is there any way to get around it?

Shlok Jhawar
  • 277
  • 2
  • 3
  • 19
  • After you signed out and go to `EnterOTPActivity` for the 2nd time, try to check this line: `userPhoneNumber = getIntent().getStringExtra("USER_MOB").trim()`. Do you still have the correct userPhoneNumber? – Harry Timothy Feb 19 '20 at 06:41
  • Yes, we do. After signing out, the user is taken back to the login screen where they re-enter their phone number. – Shlok Jhawar Feb 19 '20 at 09:40
  • Please check my answer. I think that is the root cause of your problem. – Harry Timothy Feb 19 '20 at 13:35

6 Answers6

16

If you read Firebase Docs about PhoneAuthProvider, it is clearly stated:

The verifyPhoneNumber method is reentrant: if you call it multiple times, such as in an activity's onStart method, the verifyPhoneNumber method will not send a second SMS unless the original request has timed out.

In your case, you set the timeout at 120 seconds. If you really want to test this behavior, I suggest reducing the timeout to 10 or 20 seconds, and see if you can resend OTP after the timeout.

Additionally, if you want to get the timeout event, you can add onCodeAutoRetrievalTimeOut as a part of PhoneAuthProvider.OnVerificationStateChangedCallbacks. I think it is safe to set your resendOtp enabled as true in that callback. And it is way better than defining your own timer.

Harry Timothy
  • 1,148
  • 8
  • 17
  • I tried getting the code by trying again after a couple of hours. Still no success. – Shlok Jhawar Feb 19 '20 at 14:34
  • Have you tried this: `PhoneAuthProvider.getInstance().verifyPhoneNumber(phoneNumber, 10, TimeUnit.SECONDS, this, mCallbacks);` then wait 10 second before retrying? Because I'm pretty sure the problem lies here. – Harry Timothy Feb 19 '20 at 15:11
  • Yeah, makes sense. I tried it and it worked. There's one phone number it didn't work for though, and this was one that was signed in and signed out several times over the past couple of weeks. Any way to get around this? – Shlok Jhawar Feb 20 '20 at 05:42
  • Is the phone number exist in the same device? If you are testing on different devices, make sure all of them have the same APK version (the one with the latest changes). – Harry Timothy Feb 20 '20 at 05:51
  • Yeah, the phone number is on the same device – Shlok Jhawar Feb 20 '20 at 06:58
  • I just found that this isn't the only number facing this issue. – Shlok Jhawar Feb 20 '20 at 14:27
  • @ShlokJhawar When you repetitively use a specific phone number, signing in and out for the sake of testing, without adding it to "Phone Numbers for Testing", Google might eventually block it temporarily with an error indicating unusual behavior has been detected. – mohammed youser sawwas Apr 11 '21 at 15:26
4

It's important to keep in mind that, phone numbers that you have added to "Phone Numbers for Testing" in Authentication section in Firebase console, as shown here ,will not be expected to receive any SMS OTP code. Instead, you are expected to enter the code you have already freely picked there on console.

Further situation in which "No OTP SMS is expected to receive":

When you repetitively use a specific phone number, signing in and out for the sake of testing, without adding it to "Phone Numbers for Testing", Google might eventually block it temporarily with an error indicating unusual behavior has been detected, or with error: "SMS verification code request failed: unknown status code: 17010 null".

3
@Override
public void onVerificationCompleted(PhoneAuthCredential credential) {
    // This callback will be invoked in two situations:
    // 1 - Instant verification. In some cases the phone number can be instantly
    //     verified without needing to send or enter a verification code.
    // 2 - Auto-retrieval. On some devices Google Play services can automatically
    //     detect the incoming verification SMS and perform verification without
    //     user action.
    Log.d(TAG, "onVerificationCompleted:" + credential);

    String code = phoneAuthCredential.getSmsCode();

        Log.d("REGISTER_USER", "code generated first " + code);
        if (code != null) {
            //verifying the code like in normal flow
            verifyUserAuthCode(code);
        } else {
           //you dont get any code, it is instant verification
            showProgressBar(WAIT_TITLE, VERIFYING_MESSAGE);
            signInWithPhoneAuthCredential(phoneAuthCredential);
        }
}
    private void signInWithPhoneAuthCredential(PhoneAuthCredential credential) {
          auth.signInWithCredential(credential)
            .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                @Override
                public void onComplete(@NonNull Task<AuthResult> task) {
                    final FirebaseUser user = auth.getCurrentUser();

                    if (task.isSuccessful()) {//user verified}

            });
}

Read above comments in official Google docs carefully, firebase uses Instant verification without sending phone number, for this you just need to validate "credentials" object , write logs you will find that this method will be executed everytime you send the verification code request

Prakash Reddy
  • 944
  • 10
  • 20
1

You just try to Logout from your app or one more thing you can do is to clear app data or you can reinstall.

Wahdat Jan
  • 3,988
  • 3
  • 21
  • 46
0

You have to logout your firebase user from your app.

Also if you give multiple requests from one number. firebase treats it as hacker and blocks it.

Add that of your number as tester phone number. You have to set your verification code also. Then u can use that verification code on verification screen to log in.

Sajid Zeb
  • 1,806
  • 18
  • 32
-3

If your number is verified by google play services, google would automatically sign you in without sending any SMS.

Limitless Claver
  • 479
  • 5
  • 17