Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flipcash.app.phone.components.OtpInputField
import com.flipcash.app.phone.components.SmsOtpAutofill
import com.flipcash.features.contact.verification.R
import com.getcode.theme.CodeTheme
import com.getcode.ui.components.OnWindowFocusedRequester
Expand All @@ -54,6 +55,12 @@ private fun PhoneCodeScreenContent(
val focusRequester = remember { FocusRequester() }
val keyboard = rememberKeyboardController()

SmsOtpAutofill(
otpState = state.codeTextFieldState,
otpLength = state.otpLength,
attempts = state.attempts,
)

CodeScaffold(
modifier = Modifier
.fillMaxSize()
Expand Down
2 changes: 2 additions & 0 deletions apps/flipcash/shared/phone/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ dependencies {

api(libs.rinku.compose)

implementation(libs.play.services.auth.api.phone)

testImplementation(kotlin("test"))
testImplementation(libs.bundles.unit.testing)
testImplementation(libs.robolectric)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.flipcash.app.phone.components

import android.app.Activity.RESULT_OK
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalContext
import androidx.core.os.BundleCompat
import com.getcode.utils.trace
import com.google.android.gms.auth.api.phone.SmsRetriever
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status

@Composable
fun SmsOtpAutofill(
otpState: TextFieldState,
otpLength: Int,
attempts: Int,
) {
val context = LocalContext.current
val currentOtpState by rememberUpdatedState(otpState)
val currentOtpLength by rememberUpdatedState(otpLength)
val filled = remember { mutableStateOf(false) }

val consentLauncher = rememberLauncherForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val message = result.data?.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
?: return@rememberLauncherForActivityResult
parseAndFill(message, currentOtpLength, currentOtpState, filled)
}
}

DisposableEffect(attempts) {
filled.value = false

val client = SmsRetriever.getClient(context)
client.startSmsRetriever()
.addOnSuccessListener { trace("SmsRetriever started") }
.addOnFailureListener { trace("SmsRetriever failed to start: ${it.message}") }
client.startSmsUserConsent(null)
.addOnSuccessListener { trace("SmsUserConsent started") }
.addOnFailureListener { trace("SmsUserConsent failed to start: ${it.message}") }

val filter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)

val receiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
if (intent.action != SmsRetriever.SMS_RETRIEVED_ACTION) return
val extras: Bundle = intent.extras ?: return
// GMS classes (Status) need the GMS class loader for deserialization
extras.classLoader = SmsRetriever::class.java.classLoader

val status = BundleCompat.getParcelable(extras, SmsRetriever.EXTRA_STATUS, Status::class.java)
if (status?.statusCode != CommonStatusCodes.SUCCESS) {
trace("SMS retrieval failed with status: ${status?.statusCode}")
return
}

// Retriever path: SMS message directly available
val message = extras.getString(SmsRetriever.EXTRA_SMS_MESSAGE)
if (message != null) {
trace("SmsRetriever received message")
parseAndFill(message, currentOtpLength, currentOtpState, filled)
return
}

// User Consent path: need to launch consent dialog
if (filled.value) return

val consentIntent = BundleCompat.getParcelable(
extras,
SmsRetriever.EXTRA_CONSENT_INTENT,
Intent::class.java
)
if (consentIntent != null) {
trace("Launching SMS consent dialog")
try {
consentLauncher.launch(consentIntent)
} catch (e: Exception) {
trace("Failed to launch consent dialog: ${e.message}")
}
}
}
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
context.registerReceiver(
receiver, filter, SmsRetriever.SEND_PERMISSION,
null, Context.RECEIVER_EXPORTED,
)
} else {
context.registerReceiver(receiver, filter, SmsRetriever.SEND_PERMISSION, null)
}

onDispose {
context.unregisterReceiver(receiver)
}
}
}

private fun parseAndFill(
message: String,
otpLength: Int,
state: TextFieldState,
filled: androidx.compose.runtime.MutableState<Boolean>,
) {
if (filled.value) return
val otp = """\b(\d{$otpLength})\b""".toRegex().find(message)?.groupValues?.get(1) ?: return
trace("Auto-filling OTP")
state.edit { replace(0, length, otp) }
filled.value = true
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ grpc-android = "1.82.0"
slf4j = "1.7.36"
firebase-bom = "34.14.1"
play-service-ml-barcode = "18.3.1"
play-services-auth-api-phone = "18.3.0"
google-play-billing = "9.0.0"
google-play-updates = "2.1.0"

Expand Down Expand Up @@ -220,6 +221,7 @@ firebase-perf = { module = "com.google.firebase:firebase-perf" }
# Google Play Services
play-integrity = { module = "com.google.android.play:integrity", version = "1.6.0" }
play-service-ml-barcode = { module = "com.google.android.gms:play-services-mlkit-barcode-scanning", version.ref = "play-service-ml-barcode" }
play-services-auth-api-phone = { module = "com.google.android.gms:play-services-auth-api-phone", version.ref = "play-services-auth-api-phone" }
play-services-wallet = { module = "com.google.android.gms:play-services-wallet", version = "20.0.0" }

# Google Play Billing
Expand Down
Loading