When authenticating using Firebase Auth, I want to auto input the code that is received via SMS. I am able to receive SMS and go through auth process manually, but when I use SmsRetriever, the app crashes and then the bottom sheet dialog shows up.
This is everything that that appears in Logcat:
E/FirebaseAuth: [SmsRetrieverHelper] SMS verification code request failed: unknown status code: 17010 null
Code in Fragment where user inputs their phone number:
private val SMS_CONSENT_REQUEST = 2 // Set to an unused request code
private val smsVerificationReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val smsRetrieverStatus = extras?.get(SmsRetriever.EXTRA_STATUS) as Status
when (smsRetrieverStatus.statusCode) {
CommonStatusCodes.SUCCESS -> {
// Get consent intent
val consentIntent = extras.getParcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
try {
// Start activity to show consent dialog to user, activity must be started in
// 5 minutes, otherwise you'll receive another TIMEOUT intent
startActivityForResult(consentIntent, SMS_CONSENT_REQUEST)
} catch (e: ActivityNotFoundException) {
// Handle the exception ...
}
}
CommonStatusCodes.TIMEOUT -> {
// Time out occurred, handle the error.
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val task = SmsRetriever.getClient(requireActivity()).startSmsUserConsent(null)
val intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
requireActivity().registerReceiver(smsVerificationReceiver, intentFilter)
}
override fun sendSms() {
showProgressBar(true)
SmsRetriever.getClient(requireActivity()).startSmsUserConsent(presenter.getNumber())
val options = PhoneAuthOptions.newBuilder(auth)
.setPhoneNumber(presenter.getNumber())
.setTimeout(58L, TimeUnit.SECONDS)
.setActivity(requireActivity())
.setCallbacks(callbacks)
.build()
PhoneAuthProvider.verifyPhoneNumber(options)
}
override fun onDestroy() {
super.onDestroy()
requireContext().unregisterReceiver(smsVerificationReceiver)
}
This is the code in Fragment where user has to input the code:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
// ...
SMS_CONSENT_REQUEST ->
// Obtain the phone number from the result
if (resultCode == Activity.RESULT_OK && data != null) {
// Get SMS message content
val message = data.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
// Extract one-time code from the message and complete verification
// `message` contains the entire text of the SMS message, so you will need
// to parse the string.
message?.let { presenter.parseSms(it) }
// send one time code to the server
} else {
// Consent denied. User can type OTC manually.
}
}
}
Print your FirebaseAuthException error to see what's going on. If you're using a real phone number for development and using it again and again, Firebase might block the device for a time being.
SOLUTION: Add a test phone number with a password and use it.
try to print exception in onFailure like --> {p0.message} print this line logcat and it will definately show --> E/exception in firebase: We have blocked all requests from this device due to unusual activity. Try again later. this is why because we are using this phone number many times for login
It is a too-many-request error screenshot
The solution is to either wait for few hours or -if this user is a test user- , just add the number in test users to fix the code and do not send too many SMSes.
Related
When authenticating using Firebase Auth, I want to auto input the code that is received via SMS. I am able to receive SMS and go through auth process manually, but when I use SmsRetriever, the app crashes and then the bottom sheet dialog shows up. This is everything that that appears in Logcat:
java.lang.RuntimeException: Error receiving broadcast Intent { act=com.google.android.gms.auth.api.phone.SMS_RETRIEVED flg=0x200010 pkg=com.finca.bank (has extras) } in com.google.android.gms.internal.firebase-auth-api.zzvb#45fb8c5
at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0$LoadedApk$ReceiverDispatcher$Args(LoadedApk.java:1566)
at android.app.-$$Lambda$LoadedApk$ReceiverDispatcher$Args$_BumDX2UKsnxLVrE6UJsJZkotuA.run(Unknown Source:2)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:224)
at android.app.ActivityThread.main(ActivityThread.java:7562)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'int java.lang.CharSequence.length()' on a null object reference
at java.util.regex.Matcher.reset(Matcher.java:280)
at java.util.regex.Matcher.<init>(Matcher.java:186)
at java.util.regex.Pattern.matcher(Pattern.java:1034)
at com.google.android.gms.internal.firebase-auth-api.zzvd.zzf(com.google.firebase:firebase-auth##20.0.1:1)
at com.google.android.gms.internal.firebase-auth-api.zzvb.onReceive(com.google.firebase:firebase-auth##20.0.1:8)
at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0$LoadedApk$ReceiverDispatcher$Args(LoadedApk.java:1556)
at android.app.-$$Lambda$LoadedApk$ReceiverDispatcher$Args$_BumDX2UKsnxLVrE6UJsJZkotuA.run(Unknown Source:2)
This is code inside my Fragment that receives SMS:
private val SMS_CONSENT_REQUEST = 2 // Set to an unused request code
private val smsVerificationReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
try {
if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val smsRetrieverStatus = extras?.get(SmsRetriever.EXTRA_STATUS) as Status
when (smsRetrieverStatus.statusCode) {
CommonStatusCodes.SUCCESS -> {
// Get consent intent
val consentIntent = extras.getParcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
try {
// Start activity to show consent dialog to user, activity must be started in
// 5 minutes, otherwise you'll receive another TIMEOUT intent
startActivityForResult(consentIntent, SMS_CONSENT_REQUEST)
} catch (e: ActivityNotFoundException) {
// Handle the exception ...
}
}
CommonStatusCodes.TIMEOUT -> {
// Time out occurred, handle the error.
}
}
}
} catch (e: Exception) {
Timber.e(e, "onReceive: ")
}
}
}
override fun onResume() {
super.onResume()
val task = SmsRetriever.getClient(requireActivity()).startSmsUserConsent(null)
val intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
requireActivity().registerReceiver(smsVerificationReceiver, intentFilter)
}
override fun onPause() {
super.onPause()
requireActivity().unregisterReceiver(smsVerificationReceiver)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
// ...
SMS_CONSENT_REQUEST ->
// Obtain the phone number from the result
if (resultCode == Activity.RESULT_OK && data != null) {
// Get SMS message content
val message = data.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
// Extract one-time code from the message and complete verification
// `message` contains the entire text of the SMS message, so you will need
// to parse the string.
message?.let { presenter.parseSms(it) }
// send one time code to the server
} else {
// Consent denied. User can type OTC manually.
}
}
}
Interesting thing is, the progress goes successfully in rare cases and I don't know what it depends on. Also, everything goes well in debug mode if breakpoints are set in onReceive
After several investigations, the root cause of this crash seems to be related to a conflict between Firebase Auth Instant Verification feature and SMS consent API.
In order to fix it you have two options:
Remove SMS consent API and only rely on Instant Verification
Use SMS Consent API and disable Instant Verification by setting timeout as 0. https://firebase.google.com/docs/reference/android/com/google/firebase/auth/PhoneAuthOptions.Builder#setTimeout(java.lang.Long,%20java.util.concurrent.TimeUnit))
Hope this provides some clarity around this weird issue.
Got the same error while Integrating the SMS consent API. After Searching and modification got a solution. In the below-attached image, you can see the setTimeout method consists of a parameter called timeout, change that parameter value from 60L to 0L. For more info about the method click here!
To automatically verify phone numbers, you must implement both the client and server portions of the verification flow. You just implemented the client portion. To listen to SMS and auto input the OTP code you have to also implement the server side. A complete guide is found here Perform SMS Verification on a Server
From Jan 9th, 2019 Google will remove apps from Playstore with permissions READ SMS AND CALL LOG, if they don’t explain the necessity.
Google introduced SMS Retriever API to automatically fetch a verification code sent via SMS within the app.
But those APIs are not clearly expressed and is very confusing. I don't know if it's me who thinks it is confusing. Anyhow, here is what I have looked into to read SMS but I could understand nothing.
I am not sure if this is the correct link to read SMS automatically.
https://developers.google.com/identity/sms-retriever/request
I used these dependencies
implementation 'com.google.android.gms:play-services-auth:17.0.0'
implementation 'com.google.android.gms:play-services-auth-api-phone:17.0.0'
There is one good tutorial to implement auto-read SMS but some of the APIs are deprecated so I'm trying to find any simple explanation to implement auto-read SMS in Android.
Here is the link to that tutorial
https://androidwave.com/automatic-sms-verification-android/
You should use sms retriever api for reading otp messages. Here is how you can do that.
You need below 2 dependencies for sms retrieval code
implementation 'com.google.android.gms:play-services-auth:17.0.0'
implementation 'com.google.android.gms:play-services-auth-api-phone:17.1.0'
Define few variables like this in your activity/fragment
private val SMS_CONSENT_REQUEST = 2
private lateinit var smsVerificationReceiver: BroadcastReceiver
In your onCreate() method start SMS retriever
SmsRetriever.getClient(this).startSmsUserConsent(null)
smsReceiver()
val intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
registerReceiver(smsVerificationReceiver, intentFilter)
Below is the method for broadcast receiver
private fun smsReceiver() {
smsVerificationReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val smsRetrieverStatus = extras?.get(SmsRetriever.EXTRA_STATUS) as Status
when (smsRetrieverStatus.statusCode) {
CommonStatusCodes.SUCCESS -> {
// Get consent intent
val consentIntent =
extras.getParcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
try {
// Start activity to show consent dialog to user, activity must be started in
// 5 minutes, otherwise you'll receive another TIMEOUT intent
startActivityForResult(consentIntent, SMS_CONSENT_REQUEST)
} catch (e: ActivityNotFoundException) {
// Handle the exception ...
}
}
CommonStatusCodes.TIMEOUT -> {
// Time out occurred, handle the error.
}
}
}
}
}
}
And then in onActivityResult() you can get the verification code
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
// ...
SMS_CONSENT_REQUEST ->
// Obtain the phone number from the result
if (resultCode == Activity.RESULT_OK && data != null) {
// Get SMS message content
val message = data.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
// Extract one-time code from the message and complete verification
// `message` contains the entire text of the SMS message, so you will need
// to parse the string.
val oneTimeCode = parseOneTimeCode(message) // define this function
et_otp.setText(oneTimeCode.toString())
// send one time code to the server
} else {
// Consent denied. User can type OTC manually.
}
}
}
Also don't forget to unregister receiver in onDestroy() method
unregisterReceiver(smsVerificationReceiver)
I have a thirdparty Android control that can't be used in Flutter directly. I put it in an Android activity. Then, using information from https://flutter.io/docs/development/platform-integration/platform-channels#step-3b-add-an-android-platform-specific-implementation-using-kotlin, I can successfully launch that activity and perform some actions there. The only part that doesn't work is sending results back from the activity.
Flutter code:
void showDialog() async
{
try {
final Map<String, List<double>> result = await platform.invokeMethod('show_dialog',
<String, String>{
'address': widget.user.address
});
widget.user.address = result.keys.toList()[0];
} on PlatformException catch (e) {
print('Failed to pick address: ${e.message}.');
}
}
Android code:
class MainActivity: FlutterActivity() {
private val CHANNEL = "dialog"
private lateinit var _result: MethodChannel.Result
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "show_dialog") {
_result = result
val intent = Intent(this#MainActivity, DialogActivity::class.java)
intent.putExtra("address", call.argument<String>("address"))
startActivityForResult(intent, 9689)
}
else result.notImplemented()
}
}
override fun onActivityResult(requestCode: Int, result: Int, intent: Intent?) {
if(requestCode != 9689)
return super.onActivityResult(requestCode, result, intent)
if (result == Activity.RESULT_OK) {
_result.success(mapOf(intent!!.getStringExtra("address") to
listOf(intent.getDoubleExtra("latitude", 0.0),
intent.getDoubleExtra("longitude", 0.0))))
}
else
_result.success(null)
}
}
What's the problem? Breakpoint on the line widget.user.address = result.keys.toList()[0]; is never reached, suggesting the result is never sent back.
Turns out the code was almost correct. The Android side didn't need any changes, but on the Flutter side I had to make this change:
turn
final Map<String, List<double>> result = await platform.invokeMethod(
into
final result = await platform.invokeMethod(
i. e. simply remove explicit type from the variable, because the return value of platform.invokeMethod was some kind of an internal hash map (in particular, it's name started with an underscore) rather than that of Map as specified. Flutter didn't show any errors in the console output, because for some reason it only captures Android log with debugger attached to the Android part of the application, and once I figured how to debug Android code in Android Studio, I immediately found the reason.
I'm trying to connect my game to Google Play Games Services, but when I try to login, it always returns me an error code 8 (internal error).
The code is copy pasted from Google example:
lateinit var signInClient: GoogleSignInClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_settings)
settings_login.setOnClickListener { login() }
signInClient = GoogleSignIn.getClient(this,
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN).build()
)
}
private fun login() {
startActivityForResult(signInClient.signInIntent, 9001)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != 9001) {
return
}
val task = GoogleSignIn.getSignedInAccountFromIntent(intent)
try {
val account = task.getResult(ApiException::class.java)
onConnected(account)
} catch (apiException: ApiException) {
var message: String? = apiException.message
if (message == null || message.isEmpty()) {
message = getString(R.string.signin_other_error)
}
onDisconnected()
AlertDialog.Builder(this)
.setMessage(message)
.setNeutralButton(android.R.string.ok, null)
.show()
}
}
In Google Play Console I've linked my game with debug keystore SHA-1.
I've checked everythin mentioned in Troubleshooting guide, but I still get this message again and again.
Does someone faced this issue? Any ideas how to debug it?
EDIT:
I found that it actually logs me in - if I restart game, method signInSilently() will be successful. However, it still shows this error 8 when I logout and try to log in manually. Could it be the problem with login activity overlay?
Oh, and I checked api access in Google Play Api Console - it shows that api actually receives my calls and it doesn't mention any errors.
EDIT 2: I've added requestEmail() to GoogleSignInOptions.Builder, and it shows me overlay with access request. However, it still fails in GoogleSignIn.getSignedInAccountFromIntent(intent).getResult(ApiException::class.java) with same error (8 - internal error).
It looks like this bug in Google Play Services 12.2.21:
https://github.com/googlesamples/google-services/issues/358
Google is supposed to be working on a fix for release over the air soon..
it's maybe late but I found the reason. It fixed in my case and I see your code has same problem.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
...
val task = GoogleSignIn.getSignedInAccountFromIntent(intent)
...
}
The intent you passed to the method getSignedInAccountFromIntent() is not the intent that returned by onActivityResult. The intent you passed come from activity, so you need to change it to
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
"data" is the intent returned by onActivityResult
I didn't found the reason of this error, but I found an (ugly) workaround. I noticed, that when I restart game after manual login, even if there was this error, signInSilently() method works fine, which means that API actually authenticate me and fails later. So in catch block I'm checking for status code of error, and, if it's (8 - internal error), I'm requesting last signed in account. If account is present, I assume user to be logged in.
It's really dirty but I'm out of ideas.
//onActivityResult
val task = GoogleSignIn.getSignedInAccountFromIntent(intent)
try {
val account = task.getResult(ApiException::class.java)
onSuccess(account)
} catch (apiException: ApiException) {
val acc = GoogleSignIn.getLastSignedInAccount(context)
if (apiException.statusCode == 8 && acc != null && acc.email != null) {
onSuccess(account)
} else {
onFail(apiException)
}
}
I have a client with that error. Only ONE! With a Galaxy S9. Nothing happens when clicking on the Sign In button (startActivityForResult -> GoogleSignIn.getClient.getSignInIntent)
Issue: I need to refresh the Google token used for signing into my server. Most of the time this works well, but sometimes the call to Google to get a fresh token (with a TTL of ~1hr) fails for a variety of reasons.
Desired solution: some means of retrying the call to Google that will actually work.
I have code like the following in my app:
private val googleSignInClient: GoogleSignInClient by lazy {
// This takes a measurable amount of time to compute, so do it lazily
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(WEB_CLIENT_ID) // need this to get user ID token later
.requestEmail()
.build()
GoogleSignIn.getClient(appContext, gso)
}
override fun getToken() = getRefreshedGoogleInfo()?.googleToken()
/**
* Here we "silently sign in" to get a refreshed Google ID Token.
*
* This method might block, so do not call it from the main thread.
*/
private fun getRefreshedGoogleInfo(): GoogleUserInfo? {
val task = googleSignInClient.silentSignIn()
// If the task is already complete, return the result immediately
if (task.isComplete) {
val info = task.result.toGoogleUserInfo()
Logger.v(TAG, "silentSignIn result from already-completed task = %s", info.toString())
return info
}
// If the task is not complete, await up to 5s and return result, or null
return try {
val info = task.await().toGoogleUserInfo()
Logger.v(TAG, "silentSignIn result from await task = %s", info.toString())
info
} catch (e: Exception) {
Logger.e(TAG, e, "silentSignIn result from await task = null\nerror = ${e.localizedMessage}")
null
}
}
private fun Task<GoogleSignInAccount>.await() = Tasks.await(this, 5, TimeUnit.SECONDS)
Sometimes, the call task.await() will fail because it timed out. In such a case, what is the best strategy to try again? I have tried a naive strategy of just trying again immediately up to some arbitrary numerical limit, but I have observed that if it fails the first time, it always fails on subsequent attempts. The Google docs aren't very helpful with respect to this scenario.
Instead of waiting the task up to 5 seconds, why not try again with requests and intents ? Let me show you in your code with edits.
private fun getRefreshedGoogleInfo(): GoogleUserInfo? {
val task = googleSignInClient.silentSignIn()
// If the task is already complete, return the result immediately
if (task.isComplete) {
val info = task.result.toGoogleUserInfo()
Logger.v(TAG, "silentSignIn result from already-completed task = %s", info.toString())
return info
}
else{ //Else is not necessary, but it will somehow increase readability.
// There's no immediate result ready, displays some progress indicator and waits for the async callback.
task.addOnCompleteListener(this){ signInTask ->
//We repeat the same task control again, but this time it is async.
if(signInTask.isComplete){
val info = task.result.toGoogleUserInfo() //Redone the same things in first task.isComplete scope.
Logger.v(TAG, "silentSignIn result from already-completed task = %s", info.toString())
return info
}
else{ //Again, not necessary but still.
//This is where we are gonna try again.
signInToGoogle() //Go below to see the trick.
}
}
}
}
With the code above alone, you do not have to wait for 5 seconds, but it will not try again. To try again with more persistant way ( I assume you use these functions on an activity ) :
private fun signInToGoogle(){
val signInIntent = googleSignInClient!!.signInIntent
startActivityForResult(signInIntent, RC_SIGN_IN) //You might consider checking for internet connection before calling this
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(requestCode == RC_SIGN_IN){
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
/* your code with task handle goes here */
//task.result carries the same object as above. But since this is a listener, it can not be returned with a value. So it is up to you how to handle again. I handle this with a class-wide variable which contains GoogleUserInfo?
}
}
RC_SIGN_IN is just a number for request. You could add it to your companion objects like this or use it as it is :
companion object {
private const val RC_SIGN_IN = 9001
}
Note : You could use intents for the first try too. Or if intents are too much hassle for you, you could Make function sleep for 5 seconds and call the function again.
But i strongly recommend you to use intent.