Skip to content

Commit 3441c7e

Browse files
author
Jeff Yanta
committed
Merge branch 'develop'
2 parents 2cb13ac + 28833b8 commit 3441c7e

5 files changed

Lines changed: 115 additions & 85 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.getcode.util
2+
3+
import android.content.Context
4+
import androidx.biometric.BiometricManager
5+
import androidx.biometric.BiometricPrompt
6+
import androidx.fragment.app.FragmentActivity
7+
import com.getcode.R
8+
import kotlinx.coroutines.suspendCancellableCoroutine
9+
import timber.log.Timber
10+
import java.util.concurrent.Executors
11+
import kotlin.coroutines.resume
12+
13+
class BiometricsException(
14+
val error: BiometricsError,
15+
private val messageString: CharSequence = ""
16+
) : Exception(Throwable("$messageString (${error.ordinal})")) {
17+
override val message: String
18+
get() = "${error.javaClass.simpleName} $messageString"
19+
20+
constructor(code: Int, message: CharSequence): this(BiometricsError.fromValue(code), message)
21+
}
22+
23+
enum class BiometricsError {
24+
Unknown, // BIOMETRICS_SUCCESS is 0 so using this
25+
HardwareUnavailable,
26+
UnableToProcess,
27+
Timeout,
28+
NoSpace,
29+
Cancelled,
30+
Lockout,
31+
Vendor,
32+
LockoutPermanent,
33+
UserCancelled,
34+
NoBiometrics,
35+
HardwareNotPresent,
36+
NegativeButton,
37+
NoDeviceCredential;
38+
39+
companion object {
40+
fun fromValue(code: Int): BiometricsError {
41+
return entries.toList().getOrNull(code - 1) ?: Unknown
42+
}
43+
}
44+
}
45+
46+
object Biometrics {
47+
suspend fun prompt(context: Context): Result<Unit> = suspendCancellableCoroutine { cont ->
48+
val activity = context as FragmentActivity
49+
val executor = Executors.newSingleThreadExecutor()
50+
val biometricPrompt = BiometricPrompt(
51+
activity,
52+
executor,
53+
object : BiometricPrompt.AuthenticationCallback() {
54+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
55+
Timber.d("Biometric Authentication successful")
56+
cont.resume(Result.success(Unit))
57+
}
58+
59+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
60+
val error = BiometricsException(errorCode, errString)
61+
Timber.e(t = error.cause, message = "onAuthenticationErrorForBiometrics")
62+
cont.resume(Result.failure(error))
63+
}
64+
65+
override fun onAuthenticationFailed() {
66+
Timber.e("onAuthenticationFailedForBiometrics")
67+
}
68+
})
69+
70+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
71+
.setTitle(context.getString(R.string.title_biometricAuthentication))
72+
.setDescription(context.getString(R.string.description_biometricAuthentication))
73+
.setAllowedAuthenticators(
74+
BiometricManager.Authenticators.BIOMETRIC_STRONG
75+
or BiometricManager.Authenticators.DEVICE_CREDENTIAL
76+
)
77+
.build()
78+
79+
biometricPrompt.authenticate(promptInfo)
80+
}
81+
}

app/src/main/java/com/getcode/util/Context.kt

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -27,74 +27,3 @@ fun Context.shareDownloadLink() {
2727
val intent = IntentUtils.share(url)
2828
ContextCompat.startActivity(this, intent, null)
2929
}
30-
31-
fun Context.biometricPrompt(
32-
onSuccess: () -> Unit,
33-
onError: (BiometricsException) -> Unit,
34-
) {
35-
println("biometrics prompt")
36-
val activity = this as FragmentActivity
37-
val executor = Executors.newSingleThreadExecutor()
38-
val biometricPrompt = BiometricPrompt(
39-
activity,
40-
executor,
41-
object : BiometricPrompt.AuthenticationCallback() {
42-
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
43-
Timber.d("Biometric Authentication successful")
44-
onSuccess()
45-
}
46-
47-
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
48-
val error = BiometricsException(errorCode, errString)
49-
Timber.e(t = error.cause, message = "onAuthenticationErrorForBiometrics")
50-
onError(error)
51-
}
52-
53-
override fun onAuthenticationFailed() {
54-
Timber.e("onAuthenticationFailedForBiometrics")
55-
}
56-
})
57-
58-
val promptInfo = BiometricPrompt.PromptInfo.Builder()
59-
.setTitle(getString(R.string.title_biometricAuthentication))
60-
.setDescription(getString(R.string.description_biometricAuthentication))
61-
.setNegativeButtonText(getString(R.string.action_cancel))
62-
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
63-
.build()
64-
65-
biometricPrompt.authenticate(promptInfo)
66-
}
67-
68-
69-
class BiometricsException(
70-
val error: BiometricsError,
71-
private val messageString: CharSequence = ""
72-
) : Exception(Throwable("$messageString (${error.ordinal})")) {
73-
override val message: String
74-
get() = "${error.javaClass.simpleName} $messageString"
75-
76-
constructor(code: Int, message: CharSequence): this(BiometricsError.fromValue(code), message)
77-
}
78-
79-
enum class BiometricsError {
80-
Unknown, // BIOMETRICS_SUCCESS is 0 so using this
81-
HardwareUnavailable,
82-
UnableToProcess,
83-
Timeout,
84-
NoSpace,
85-
Cancelled,
86-
Lockout,
87-
Vendor,
88-
LockoutPermanent,
89-
UserCancelled,
90-
NoBiometrics,
91-
HardwareNotPresent,
92-
NegativeButton,
93-
NoDeviceCredential;
94-
95-
companion object {
96-
fun fromValue(code: Int): BiometricsError {
97-
return entries.toList().getOrNull(code - 1) ?: Unknown
98-
}
99-
}
100-
}

app/src/main/java/com/getcode/view/main/home/HomeScan.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,17 +167,18 @@ private fun HomeScan(
167167
mutableStateOf(requestPayload)
168168
}
169169

170-
LaunchedEffect(previewing, dataState.balance, deepLinkSaved, requestPayloadSaved) {
170+
val biometricsState = LocalBiometricsState.current
171+
LaunchedEffect(biometricsState, previewing, dataState.balance, deepLinkSaved, requestPayloadSaved) {
171172
if (previewing) {
172173
focusManager.clearFocus()
173174
}
174175

175-
if (!deepLinkSaved.isNullOrBlank()) {
176+
if (biometricsState.passed && !deepLinkSaved.isNullOrBlank()) {
176177
homeViewModel.openCashLink(deepLink)
177178
deepLinkSaved = null
178179
}
179180

180-
if (!requestPayloadSaved.isNullOrBlank() && dataState.balance != null) {
181+
if (biometricsState.passed && !requestPayloadSaved.isNullOrBlank() && dataState.balance != null) {
181182
delay(500.milliseconds)
182183
homeViewModel.handleRequest(requestPayload)
183184
requestPayloadSaved = null

app/src/main/java/com/getcode/view/main/home/components/BiometricsBlockingView.kt

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ import com.getcode.R
3232
import com.getcode.theme.BrandOverlay
3333
import com.getcode.theme.CodeTheme
3434
import com.getcode.ui.components.OnLifecycleEvent
35+
import com.getcode.util.Biometrics
3536
import com.getcode.util.BiometricsError
36-
import com.getcode.util.biometricPrompt
37+
import com.getcode.util.BiometricsException
3738
import timber.log.Timber
3839

3940
@Composable
@@ -114,19 +115,21 @@ internal fun rememberBiometricsState(
114115

115116
LaunchedEffect(checkBiometrics, requireBiometrics) {
116117
if (checkBiometrics && requireBiometrics == true) {
117-
context.biometricPrompt(
118-
onSuccess = {
119-
biometricsPassed = true
120-
checkBiometrics = false
121-
},
122-
onError = {
123-
if (it.error == BiometricsError.NoBiometrics) {
124-
Timber.e("missing biometrics")
125-
onBiometricsNotEnrolled()
118+
Biometrics.prompt(context)
119+
.onFailure {
120+
val error = it as? BiometricsException
121+
error?.let { exception ->
122+
if (exception.error == BiometricsError.NoBiometrics) {
123+
Timber.e("missing biometrics")
124+
onBiometricsNotEnrolled()
125+
}
126126
}
127127
checkBiometrics = false
128128
}
129-
)
129+
.onSuccess {
130+
biometricsPassed = true
131+
checkBiometrics = false
132+
}
130133
}
131134
}
132135

app/src/main/java/com/getcode/view/main/home/components/CodeScanner.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,21 @@ import androidx.compose.foundation.background
1515
import androidx.compose.foundation.layout.Box
1616
import androidx.compose.foundation.layout.fillMaxSize
1717
import androidx.compose.runtime.Composable
18+
import androidx.compose.runtime.DisposableEffect
1819
import androidx.compose.runtime.LaunchedEffect
1920
import androidx.compose.runtime.getValue
2021
import androidx.compose.runtime.mutableStateOf
2122
import androidx.compose.runtime.remember
23+
import androidx.compose.runtime.rememberCoroutineScope
2224
import androidx.compose.runtime.setValue
2325
import androidx.compose.ui.Modifier
2426
import androidx.compose.ui.platform.LocalContext
2527
import androidx.compose.ui.platform.LocalLifecycleOwner
2628
import androidx.compose.ui.viewinterop.AndroidView
29+
import androidx.lifecycle.Lifecycle
2730
import androidx.lifecycle.asFlow
2831
import com.getcode.theme.CodeTheme
32+
import com.getcode.ui.components.OnLifecycleEvent
2933
import com.getcode.ui.utils.AnimationUtils
3034
import com.getcode.utils.trace
3135
import com.kik.kikx.kikcodes.implementation.KikCodeAnalyzer
@@ -35,6 +39,7 @@ import kotlinx.coroutines.Dispatchers
3539
import kotlinx.coroutines.flow.distinctUntilChanged
3640
import kotlinx.coroutines.flow.launchIn
3741
import kotlinx.coroutines.flow.onEach
42+
import kotlinx.coroutines.launch
3843
import kotlinx.coroutines.withContext
3944
import timber.log.Timber
4045
import java.util.concurrent.Executors
@@ -77,12 +82,23 @@ fun CodeScanner(
7782
KikCodeAnalyzer(scanner, onCodeScanned)
7883
}
7984

85+
val scope = rememberCoroutineScope()
8086
LaunchedEffect(scanner) {
8187
val cameraProvider = context.getCameraProvider()
8288
cameraProvider.unbindAll()
8389
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
8490
}
8591

92+
OnLifecycleEvent { _, event ->
93+
if (event == Lifecycle.Event.ON_STOP) {
94+
scope.launch {
95+
val cameraProvider = context.getCameraProvider()
96+
cameraProvider.unbindAll()
97+
}
98+
}
99+
100+
}
101+
86102
var streamState by remember(previewView) {
87103
mutableStateOf(PreviewView.StreamState.IDLE)
88104
}

0 commit comments

Comments
 (0)