NewPartizal
NewPartizal

Reputation: 1124

How to use google sms retriever api with jetpack compose in kotlin?

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

Answers (1)

Enes Kayıklık
Enes Kayıklık

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

Related Questions