1
1
/*
2
- * Copyright 2024 ACINQ SAS
2
+ * Copyright 2025 ACINQ SAS
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
14
14
* limitations under the License.
15
15
*/
16
16
17
- package fr.acinq.phoenix.android.components.screenlock
17
+ package fr.acinq.phoenix.android.components.auth.pincode
18
18
19
19
import android.content.Context
20
20
import androidx.compose.runtime.getValue
21
21
import androidx.compose.runtime.mutableStateOf
22
22
import androidx.compose.runtime.setValue
23
23
import androidx.lifecycle.ViewModel
24
- import androidx.lifecycle.ViewModelProvider
25
24
import androidx.lifecycle.viewModelScope
26
- import androidx.lifecycle.viewmodel.CreationExtras
27
- import fr.acinq.phoenix.android.PhoenixApplication
28
- import fr.acinq.phoenix.android.components.screenlock.PinDialog.PIN_LENGTH
29
- import fr.acinq.phoenix.android.security.EncryptedPin
30
- import fr.acinq.phoenix.android.utils.datastore.UserPrefsRepository
25
+ import fr.acinq.phoenix.android.components.auth.pincode.PinDialog.PIN_LENGTH
31
26
import kotlinx.coroutines.Dispatchers
32
27
import kotlinx.coroutines.cancelAndJoin
33
28
import kotlinx.coroutines.delay
34
- import kotlinx.coroutines.flow.first
35
29
import kotlinx.coroutines.flow.flow
36
30
import kotlinx.coroutines.launch
31
+ import org.slf4j.Logger
37
32
import org.slf4j.LoggerFactory
38
33
import kotlin.time.Duration
39
34
import kotlin.time.Duration.Companion.minutes
40
35
import kotlin.time.Duration.Companion.seconds
41
36
42
- /* * View model tracking the state of the PIN dialog UI. */
43
- sealed class CheckPinFlowState {
44
-
45
- data object Init : CheckPinFlowState ()
46
37
47
- data class Locked (val timeToWait : Duration ): CheckPinFlowState()
48
- data object CanType : CheckPinFlowState ()
49
- data object Checking : CheckPinFlowState ()
50
- data object MalformedInput : CheckPinFlowState ()
51
- data object IncorrectPin : CheckPinFlowState ()
52
- data class Error (val cause : Throwable ) : CheckPinFlowState()
38
+ /* * View model tracking the state of the PIN dialog UI. */
39
+ sealed class CheckPinState {
40
+ data object Init : CheckPinState ()
41
+ data class Locked (val timeToWait : Duration ): CheckPinState()
42
+ data object CanType : CheckPinState ()
43
+ data object Checking : CheckPinState ()
44
+ data object MalformedInput : CheckPinState ()
45
+ data object IncorrectPin : CheckPinState ()
46
+ data class Error (val cause : Throwable ) : CheckPinState()
53
47
}
54
48
55
- class CheckPinFlowViewModel ( private val userPrefsRepository : UserPrefsRepository ) : ViewModel() {
56
- private val log = LoggerFactory .getLogger(this ::class .java)
57
- var state by mutableStateOf<CheckPinFlowState >( CheckPinFlowState .Init )
49
+ abstract class CheckPinViewModel : ViewModel () {
50
+ val log: Logger = LoggerFactory .getLogger(this ::class .java)
51
+ var state by mutableStateOf<CheckPinState >( CheckPinState .Init )
58
52
private set
59
53
60
54
var pinInput by mutableStateOf(" " )
61
55
62
- init {
63
- viewModelScope.launch { evaluateLockState() }
64
- }
56
+ abstract suspend fun getPinCodeAttempt (): Int
57
+ abstract suspend fun savePinCodeSuccess ()
58
+ abstract suspend fun savePinCodeFailure ()
59
+ abstract suspend fun getExpectedPin (context : Context ): String?
65
60
66
- private suspend fun evaluateLockState () {
67
- val currentPinCodeAttempt = userPrefsRepository. getPinCodeAttempt.first ()
61
+ suspend fun evaluateLockState () {
62
+ val currentPinCodeAttempt = getPinCodeAttempt()
68
63
val timeToWait = when (currentPinCodeAttempt) {
69
64
0 , 1 , 2 -> Duration .ZERO
70
65
3 -> 10 .seconds
71
- 4 -> 1 .minutes
72
- 5 -> 2 .minutes
73
- 6 -> 5 .minutes
74
- 7 -> 10 .minutes
66
+ 4 -> 30 .seconds
67
+ 5 -> 1 .minutes
68
+ 6 -> 2 .minutes
69
+ 7 -> 5 .minutes
70
+ 8 -> 10 .minutes
75
71
else -> 30 .minutes
76
72
}
73
+
77
74
if (timeToWait > Duration .ZERO ) {
78
- state = CheckPinFlowState .Locked (timeToWait)
75
+ state = CheckPinState .Locked (timeToWait)
79
76
val countdownJob = viewModelScope.launch {
80
77
val countdownFlow = flow {
81
78
while (true ) {
@@ -85,70 +82,60 @@ class CheckPinFlowViewModel(private val userPrefsRepository: UserPrefsRepository
85
82
}
86
83
countdownFlow.collect {
87
84
val s = state
88
- if (s is CheckPinFlowState .Locked ) {
89
- state = CheckPinFlowState .Locked ((s.timeToWait.minus(1 .seconds)).coerceAtLeast(Duration .ZERO ))
85
+ if (s is CheckPinState .Locked ) {
86
+ state = CheckPinState .Locked ((s.timeToWait.minus(1 .seconds)).coerceAtLeast(Duration .ZERO ))
90
87
}
91
88
}
92
89
}
93
90
delay(timeToWait)
94
91
countdownJob.cancelAndJoin()
95
- state = CheckPinFlowState .CanType
92
+ state = CheckPinState .CanType
96
93
} else {
97
- state = CheckPinFlowState .CanType
94
+ state = CheckPinState .CanType
98
95
}
99
96
}
100
97
101
98
fun checkPinAndSaveOutcome (context : Context , pin : String , onPinValid : () -> Unit ) {
102
- if (state is CheckPinFlowState .Checking || state is CheckPinFlowState .Locked ) return
103
- state = CheckPinFlowState .Checking
99
+ if (state is CheckPinState .Checking || state is CheckPinState .Locked ) return
100
+ state = CheckPinState .Checking
104
101
105
102
viewModelScope.launch(Dispatchers .IO ) {
106
103
try {
107
104
if (pin.isBlank() || pin.length != PIN_LENGTH ) {
108
105
log.debug(" malformed pin" )
109
- state = CheckPinFlowState .MalformedInput
106
+ state = CheckPinState .MalformedInput
110
107
delay(1300 )
111
- if (state is CheckPinFlowState .MalformedInput ) {
108
+ if (state is CheckPinState .MalformedInput ) {
112
109
evaluateLockState()
113
110
}
114
111
}
115
112
116
- val expected = EncryptedPin .getPinFromDisk (context)
113
+ val expected = getExpectedPin (context)
117
114
if (pin == expected) {
118
115
log.debug(" valid pin" )
119
116
delay(20 )
120
- userPrefsRepository. savePinCodeSuccess()
117
+ savePinCodeSuccess()
121
118
pinInput = " "
122
- state = CheckPinFlowState .CanType
119
+ state = CheckPinState .CanType
123
120
viewModelScope.launch(Dispatchers .Main ) {
124
121
onPinValid()
125
122
}
126
123
} else {
127
124
log.debug(" incorrect pin" )
128
125
delay(80 )
129
- userPrefsRepository. savePinCodeFailure()
130
- state = CheckPinFlowState .IncorrectPin
126
+ savePinCodeFailure()
127
+ state = CheckPinState .IncorrectPin
131
128
delay(1300 )
132
129
pinInput = " "
133
130
evaluateLockState()
134
131
}
135
132
} catch (e: Exception ) {
136
133
log.error(" error when checking pin code: " , e)
137
- state = CheckPinFlowState .Error (e)
134
+ state = CheckPinState .Error (e)
138
135
delay(1300 )
139
136
pinInput = " "
140
137
evaluateLockState()
141
138
}
142
139
}
143
140
}
144
-
145
- companion object {
146
- val Factory : ViewModelProvider .Factory = object : ViewModelProvider .Factory {
147
- @Suppress(" UNCHECKED_CAST" )
148
- override fun <T : ViewModel > create (modelClass : Class <T >, extras : CreationExtras ): T {
149
- val application = checkNotNull(extras[ViewModelProvider .AndroidViewModelFactory .APPLICATION_KEY ] as ? PhoenixApplication )
150
- return CheckPinFlowViewModel (application.userPrefs) as T
151
- }
152
- }
153
- }
154
141
}
0 commit comments