Reputation: 1124
I want to detect a 4-digit sms code from the service and auto-fill the otp boxes I make. I used google documentation for this, the source I used is:
https://developers.google.com/identity/sms-retriever/request
I write with jetpack compose, so I am having a little difficulty adapting it. I have more than one screen, but the operation is briefly as follows, it comes to a screen where the user information and phone number are entered, after entering the information, I press the continue button and when I press the button, I send the user's information and phone number to the service, an sms is sent from the service to the user's phone number. Of course, when he presses the continue button, he goes to the other screen. There are 4 boxes on the verification screen, I want these boxes to be filled automatically when the user's phone receives an sms. I followed the path in the google documentation of this, I tried to do the things written on the internet, but I could not find a result because some did not do it with compose and some did not share working code and some are out of date.
I briefly explained what I want to do, it's actually something that happens in most applications, but I don't know because I have never done anything like this before and I don't have much experience, so it would be great if he guides me in detail, at least by supporting it with code.
here are the codes i made
screen where I want user information, phone number, name, surname, etc.
@Composable
fun UserFormScreenRoute(
sharedViewModel: SharedViewModel,
viewModel: UserFormViewModel = hiltViewModel()
) {
val state by viewModel.state.collectAsState()
UserFormScreen(
sharedViewModel = sharedViewModel,
state = state,
postRegister = viewModel::postRegister,
...
)
}
@Composable
fun UserFormScreen(
sharedViewModel: SharedViewModel,
state: UserFormScreenState,
postRegister: (RegisterUiModel) -> Unit,
...
) {
.
.
. // other informations and other components etc. it is not very important
DYTLoginAndContinueButton(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = stringResource(id = R.string.devam),
navController = null,
route = null,
enabled = buttonEnabled(state)
) {
postRegister(
RegisterUiModel(
email = "",
phone = getOnlyPhoneNumber(),
name = state.name,
birthdate = state.birthDate,
gender = state.gender.get(),
password = state.password,
eatingHabit = sharedViewModel.registerData?.eatingHabit,
wellnessGoal = sharedViewModel.registerData?.wellnessGoal,
token = "xxx",
dialCode = "90"//state.phoneCode.substring(1) ?: "90"
)
)
}
}
DYTLoginAndContinueButton is a button. it navigates verification code screen when user click this button and send user's phone number to the service to send sms code to user
hear is my code verification screen
@Composable
fun VerificationPhoneCodeRoute(
navHostController: NavHostController,
viewModel: VerificationPhoneCodeViewModel = hiltViewModel()
) {
val state by viewModel.state.collectAsState()
VerificationPhoneCodeScreen(
navHostController = navHostController,
state = state,
onChangeOtpValue = viewModel::onChangeOtpValue,
verificationSmsCode = viewModel::verificationSmsCode,
setError = viewModel::setError,
)
}
@Composable
fun VerificationPhoneCodeScreen(
navHostController: NavHostController,
state: VerificationPhoneCodeScreenState,
onChangeOtpValue: (String) -> Unit,
verificationSmsCode: (String) -> Unit,
setError: (Boolean) -> Unit
) {
if (state.error) {
CustomAlertDialog(
text = "error!",
ButtonText = "okey",
onChangeError = {
setError(it)
}
)
}
Scaffold(
topBar = {
BackPopUp(
navController = navHostController,
route = RegisterScreen.SignUpUserFormScreen.route
)
},
backgroundColor = Color.Transparent
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
Text(
textAlign = TextAlign.Center,
modifier = Modifier.padding(10.dp),
text = please enter your sms code below field,
style = MaterialTheme.typography.subtitle1
)
OtpTextField(
otpText = state.otpValue,
otpVerificationStatus = state.otpVerificationStatus,
onOtpTextChange = { value, otpInputFilled ->
onChangeOtpValue(value)
if (otpInputFilled) {
verificationSmsCode(value)
}
}
)
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = state.phoneNumber
)
}
}
}
OtpTextField
@Composable
fun OtpTextField(
modifier: Modifier = Modifier,
otpVerificationStatus: OtpVerificationStatus,
otpText: String,
otpCount: Int = 4,
onOtpTextChange: (String, Boolean) -> Unit
) {
LaunchedEffect(Unit) {
if (otpText.length > otpCount) {
throw IllegalArgumentException("Otp text value must not have more than otpCount: $otpCount characters")
}
}
BasicTextField(
modifier = modifier,
value = TextFieldValue(otpText, selection = TextRange(otpText.length)),
onValueChange = {
if (it.text.length <= otpCount) {
onOtpTextChange.invoke(it.text, it.text.length == otpCount)
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
decorationBox = {
Row(
modifier = Modifier.fillMaxWidth().padding(15.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
repeat(otpCount) { index ->
CharView(
index = index,
text = otpText,
otpVerificationStatus
)
Spacer(modifier = Modifier.width(8.dp))
}
}
}
)
}
@Composable
private fun CharView(
index: Int,
text: String,
otpVerificationStatus: OtpVerificationStatus
) {
val isFocused = text.length == index
val char = when {
index == text.length -> "0"
index > text.length -> ""
else -> text[index].toString()
}
var focusedColor = MaterialTheme.colors.DYTThemeColor
var nonFocusedColor = MaterialTheme.colors.grayColor
when(otpVerificationStatus){
OtpVerificationStatus.DEFAULT -> {
focusedColor = MaterialTheme.colors.DYTThemeColor
nonFocusedColor = MaterialTheme.colors.grayColor
}
OtpVerificationStatus.VERIFICATION_TRUE -> {
focusedColor = DYTGreenColor
nonFocusedColor = DYTGreenColor
}
OtpVerificationStatus.VERIFICATION_FALSE -> {
focusedColor = red_color
nonFocusedColor = red_color
}
}
Text(
modifier = Modifier
.size(50.dp, 60.dp)
.border(
2.dp, when {
isFocused -> focusedColor
else -> nonFocusedColor
}, RoundedCornerShape(8.dp)
)
.padding(2.dp)
.offset(y = 15.dp),
text = char,
style = MaterialTheme.typography.h4,
color = MaterialTheme.colors.DYTThemeColor,
textAlign = TextAlign.Center
)
}
MySMSBroadcastReceiver
class MySMSBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val status = extras?.get(SmsRetriever.EXTRA_STATUS) as Status
when (status.statusCode) {
CommonStatusCodes.SUCCESS -> {
val message = extras.get(SmsRetriever.EXTRA_SMS_MESSAGE) as String
println("SMS CODE MESSAGE -> "+message)
}
CommonStatusCodes.TIMEOUT -> {
}
}
}
}
}
Manifest
I added this code into manifest application tag
<receiver android:name=".diyetkolikfeature.ui.register.signup.verificationphonecodescren.components.MySMSBroadcastReceiver" android:exported="true"
android:permission="com.google.android.gms.auth.api.phone.permission.SEND">
<intent-filter>
<action android:name="com.google.android.gms.auth.api.phone.SMS_RETRIEVED"/>
</intent-filter>
</receiver>
Build.gradle
I added these too in my build.gradle dependencies
implementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
implementation 'com.google.android.gms:play-services-auth:20.4.1'
MainActivity
I thought it would be right to add mainactivity because I don't know where to install this part.
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
lateinit var navHostController: NavHostController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val client = SmsRetriever.getClient(this)
val task: Task<Void> = client.startSmsRetriever()
task.addOnSuccessListener(OnSuccessListener<Void?> {
})
task.addOnFailureListener(OnFailureListener {
})
setContent {
DiyetkolikTheme {
navHostController = rememberNavController()
Surface(
modifier = Modifier
.fillMaxSize()
) {
RootNavGraph(navHostController = navHostController)
}
}
}
}
}
Upvotes: 1
Views: 1468
Reputation: 599
Little bit late answer but in case who are searching the same question i hope the answer will help.
Instead of using Automatic SMS verification
you can use Request one-time consent to read an SMS verification code
. Here is the official documentation.
I wrote a article about it. You can check for more information.
Firstly Sms Retriever Api implementation step is correct. But instead of startSmsRetriever
you have to use startSmsUserConsent
. It listens a broadcast for next 5 minutes.
Now lets look at the broadcast receiver. We can listen for broadcast inside a @Composable
function. For this purpoes i wrote a SystemBroadcastReceiver
helper function.
@Composable
fun SystemBroadcastReceiver(
systemAction: String,
onSystemEvent: (intent: Intent?) -> Unit
) {
val context = LocalContext.current
// If either context or systemAction changes, unregister and register again
DisposableEffect(context, systemAction) {
val intentFilter = IntentFilter(systemAction)
val broadcast = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
onSystemEvent(intent)
}
}
/*
As discussed at Google I/O 2023, registering receivers
with intention using the RECEIVER_EXPORTED / RECEIVER_NOT_EXPORTED
flag was introduced as part of Android 13 and is now a requirement
for apps running on Android 14 or higher (U+).
https://stackoverflow.com/a/77276774/13447094
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
context.registerReceiver(broadcast, intentFilter, Context.RECEIVER_EXPORTED)
} else {
context.registerReceiver(broadcast, intentFilter)
}
// When the effect leaves the Composition, remove the callback
onDispose {
context.unregisterReceiver(broadcast)
}
}
}
We can call this function inside any composable with providing systemAction
that we want to listen.
In our case we want to listen SMS_RETRIEVED_ACTION
. So the code will be
SystemBroadcastReceiver(
systemAction = SmsRetriever.SMS_RETRIEVED_ACTION,
) { intent ->
val extras = intent?.extras
val smsRetrieverStatus = extras?.get(SmsRetriever.EXTRA_STATUS) as Status
when (smsRetrieverStatus.statusCode) {
CommonStatusCodes.SUCCESS -> {
val consentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
extras.getParcelable(SmsRetriever.EXTRA_CONSENT_INTENT, Intent::class.java)
} else {
extras.getParcelable(SmsRetriever.EXTRA_CONSENT_INTENT)
}
if (consentIntent != null) {
smsRetrieverLauncher.launch(consentIntent)
}
}
}
}
Let's define smsRetrieverLauncher
. Inside the top of your Composable define the result launcher.
val smsRetrieverLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val data = it.data
if (it.resultCode != Activity.RESULT_OK || data == null) return@rememberLauncherForActivityResult
val message = data.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
// You can extract otpCode from the whole message text.
// In my case the OTP code at the end of the message.
// Example message: "Your verification code is 784512 OperatorCode"
val otpCode = message?.split(" ")?.reversed()?.get(1)
// Set otpCode to your text field. Since we are inside a Composable
// you can pass by using TextFieldState or whatever that you are using depending on your case.
}
Update your VerificationPhoneCodeScreen
as below
@Composable
fun VerificationPhoneCodeScreen(
navHostController: NavHostController,
state: VerificationPhoneCodeScreenState,
onChangeOtpValue: (String) -> Unit,
verificationSmsCode: (String) -> Unit,
setError: (Boolean) -> Unit
) {
val smsRetrieverLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val data = it.data
if (it.resultCode != Activity.RESULT_OK || data == null) return@rememberLauncherForActivityResult
val message = data.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
// Update the otpCode retriever logic.
val otpCode = message?.split(" ")?.reversed()?.get(1)
onChangeOtpValue(otpCode)
}
SystemBroadcastReceiver(
systemAction = SmsRetriever.SMS_RETRIEVED_ACTION,
) { intent ->
val extras = intent?.extras
val smsRetrieverStatus = extras?.get(SmsRetriever.EXTRA_STATUS) as Status
when (smsRetrieverStatus.statusCode) {
CommonStatusCodes.SUCCESS -> {
val consentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
extras.getParcelable(SmsRetriever.EXTRA_CONSENT_INTENT, Intent::class.java)
} else {
extras.getParcelable(SmsRetriever.EXTRA_CONSENT_INTENT)
}
if (consentIntent != null) {
smsRetrieverLauncher.launch(consentIntent)
}
}
}
}
if (state.error) {
CustomAlertDialog(
text = "error!",
ButtonText = "okey",
onChangeError = {
setError(it)
}
)
}
Scaffold(
topBar = {
BackPopUp(
navController = navHostController,
route = RegisterScreen.SignUpUserFormScreen.route
)
},
backgroundColor = Color.Transparent
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
Text(
textAlign = TextAlign.Center,
modifier = Modifier.padding(10.dp),
text = please enter your sms code below field,
style = MaterialTheme.typography.subtitle1
)
OtpTextField(
otpText = state.otpValue,
otpVerificationStatus = state.otpVerificationStatus,
onOtpTextChange = { value, otpInputFilled ->
onChangeOtpValue(value)
if (otpInputFilled) {
verificationSmsCode(value)
}
}
)
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = state.phoneNumber
)
}
}
}
I hope it helps. Don't forget to check my Medium article for more information.
Upvotes: 0