Blog Infos
Author
Published
Topics
, , , ,
Published
Photo by Rohit Tandon on Unsplash

 

One-Time Password (OTP) is a security code for single use during a login or transaction processes, providing an added layer of protection to minimize the risk of fake requests and improve security. So, applications use OTP to sure that each login or transaction is verified with a unique code, improving security by preventing unauthorized access and reducing the possible fake activities.

If you just need the code, you can find this solution as a library here.

To start integrating automatic OTP message reading, we first need to choose the appropriate API. Google provides two options, and the image below will help us decide which one to use.

OTP Reader APIs Provided by Google

 

Generally, OTPs are 4, 5, or 6 digits long, we do not need to have sender in our contact list and we want user interaction to accept OTP. Therefore, we will use the SMS User Consent API.

Implementation

To start with the SMS User Consent API we need to add Google Mobile Services library to our project

implementation("com.google.android.gms:play-services-auth:21.2.0")

Next, we need to initialize the SMS Retriever client. We do this inside a LaunchedEffect block to avoid unnecessary reinitializations.

LaunchedEffect(key1 = true) {
SmsRetriever
.getClient(context)
.startSmsUserConsent(null)
.addOnSuccessListener {
shouldRegisterReceiver = true
}
}

We need to use ActivityResultLauncher to handle the result of the SMS Consent API. This launcher listens for the result from the SMS retriever, processes the SMS message. If the consent was granted extracts the verification code. If the consent is denied, it triggers an error handler.

private fun getVerificationCodeFromSms(message: String, smsCodeLength: Int): String =
message.filter { it.isDigit() }.take(smsCodeLength)

We need to register a broadcast receiver and manage the lifecycle of the receiver. Jetpack Compose provides a DisposableEffect API that allows us to clean up when a composable leaves the composition. Here’s how we use it. Also you can see the base code over the link above.

@Composable
internal fun SystemBroadcastReceiver(
systemAction: String,
onSystemEvent: (intent: Intent?) -> Unit,
) {
val context = LocalContext.current
val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)
// 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?) {
currentOnSystemEvent(intent)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(broadcast, intentFilter, RECEIVER_EXPORTED)
} else {
context.registerReceiver(broadcast, intentFilter)
}
// Unregister the broadcast receiver when the effect leaves the Composition
onDispose {
context.unregisterReceiver(broadcast)
}
}
}

We need to handle the received intent and trigger the consent dialog if applicable. The SystemBroadcastReceiver will provide us with the received SMS.

if (shouldRegisterReceiver) {
SystemBroadcastReceiver(systemAction = SmsRetriever.SMS_RETRIEVED_ACTION) { intent ->
if (intent != null && SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val smsRetrieverStatus = extras?.parcelable<Status>(SmsRetriever.EXTRA_STATUS) as Status
when (smsRetrieverStatus.statusCode) {
CommonStatusCodes.SUCCESS -> {
val consentIntent = extras.parcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
try {
// Start consent dialog. Timeout intent will be sent after 5 minutes
consentIntent?.let { launcher.launch(it) }
} catch (e: ActivityNotFoundException) {
onError(context.getString(R.string.activity_not_found_error))
}
}
CommonStatusCodes.TIMEOUT -> onError(context.getString(R.string.sms_timeout_error))
}
}
}
}

The getParcelable function are now deprecated, so we can write a simple extension function for Bundle to retrieve it using BundleCompat instead.

internal inline fun <reified T : Parcelable> Bundle.parcelable(key: String): T? =
BundleCompat.getParcelable(this, key, T::class.java)

We are ready to use SMS Consent API in action. I combined above codes into a composable function.

@Composable
fun SmsUserConsent(
smsCodeLength: Int,
onOTPReceived: (otp: String) -> Unit,
onError: (error: String) -> Unit,
) {
val context = LocalContext.current
var shouldRegisterReceiver by remember { mutableStateOf(false) }
LaunchedEffect(key1 = true) {
SmsRetriever
.getClient(context)
.startSmsUserConsent(null)
.addOnSuccessListener {
shouldRegisterReceiver = true
}
}
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK && it.data != null) {
val message: String? = it.data!!.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
message?.let {
val verificationCode = getVerificationCodeFromSms(message, smsCodeLength)
onOTPReceived(verificationCode)
}
shouldRegisterReceiver = false
} else {
onError(context.getString(R.string.sms_retriever_error_consent_denied))
}
}
if (shouldRegisterReceiver) {
SystemBroadcastReceiver(systemAction = SmsRetriever.SMS_RETRIEVED_ACTION) { intent ->
if (intent != null && SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val smsRetrieverStatus = extras?.parcelable<Status>(SmsRetriever.EXTRA_STATUS) as Status
when (smsRetrieverStatus.statusCode) {
CommonStatusCodes.SUCCESS -> {
val consentIntent = extras.parcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
try {
// Start consent dialog. Timeout intent will be sent after 5 minutes
consentIntent?.let { launcher.launch(it) }
} catch (e: ActivityNotFoundException) {
onError(context.getString(R.string.activity_not_found_error))
}
}
CommonStatusCodes.TIMEOUT -> onError(context.getString(R.string.sms_timeout_error))
}
}
}
}
}

I use the function at the screen level. For example, if you’re implementing this, you probably have an OTP Verification Screen. Here’s a sample implementation using a VerificationScreen composable.”

It is not that long but if you do not want to deal with all of these you can use this as a library.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

BONUS: GitHub Actions For Automation

I shared this OTP reader process as a library and you can integrate it into your application with just one function call. I want to tell how to publish a new release and tag on GitHub by using GitHub Actions as a bonus.

First, add the following folder structure to the root directory: .github/workflows. You can name the YAML file whatever you prefer.

Yes, I still use the old UI

 

  1. The events that trigger the workflow must be specified in the YAML file. We want to trigger the workflow when code is pushed to the development branch.
on:
push:
branches:
- 'development'
view raw on-push.yml hosted with ❤ by GitHub

Alternatively, you might want to trigger the workflow when a pull request is merged into the main branch. However, this approach did not work as expected. All jobs ran except for the new tag and release.

on:
pull_request:
types: [closed]
branches:
- 'main'
view raw on-merge.yml hosted with ❤ by GitHub

2. Permissions must be given to push new tag and release.

permissions:
contents: write
view raw permissions.yml hosted with ❤ by GitHub

3. We need to define jobs that tell GitHub Actions to perform. We will start with a condition to run the job when code is pushed to the development branch and merged into main. If you just want to trigger workflow on push to development you do not need to add this condition.

jobs:
create-new-tag-and-release:
if: ${{ github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' && github.event.pull_request.head.ref == 'development' }}
runs-on: ubuntu-latest

4. Now the things start actually. Firstly we need to checkout out the repository. fetch-dept means that number of commits to fetch. 0 indicates all history for all branches and tags.

- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
view raw checkout.yml hosted with ❤ by GitHub

5. Get the next version of tag and push it.

name: Get next version
id: get_next_version
uses: thenativeweb/get-next-version@2.6.2
- name: Push new tag
if: ${{ steps.get_next_version.outputs.hasNextVersion == 'true' && steps.check_tag.outcome != 'failure' }}
run: |
git tag ${{ steps.get_next_version.outputs.version }}
git push origin ${{ steps.get_next_version.outputs.version }}

6. Read the release note and proceed the new release. You can set prerelease to true if you just want to release a beta version. If you are using development it can be set to true.

- name: Read release body from file
id: read_release_body
run: |
echo "RELEASE_BODY=$(cat release-notes.txt)" >> $GITHUB_ENV
- name: Create GitHub Release
if: ${{ steps.get_next_version.outputs.hasNextVersion == 'true' && steps.check_tag.outcome != 'failure' }}
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.get_next_version.outputs.version }}
release_name: 'Auto OTP Reader ${{ steps.get_next_version.outputs.version }}'
body: ${{ env.RELEASE_BODY }}
draft: false
prerelease: false

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE
blog
The suspension capability is the most essential feature upon which all other Kotlin Coroutines…
READ MORE
Menu