Skip to content

Commit c1bfb50

Browse files
fix: disable resend code button (cherrypick from rc) (#WPB-18364) (#4194)
Co-authored-by: Yamil Medina <yamilmedina@users.noreply.github.com>
1 parent 3ba91b4 commit c1bfb50

File tree

13 files changed

+212
-17
lines changed

13 files changed

+212
-17
lines changed

app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeScreen.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,11 @@ private fun CodeContent(
186186
modifier = Modifier.padding(vertical = MaterialTheme.wireDimensions.spacing16x)
187187
)
188188
}
189-
ResendCodeText(onResendCodePressed = onResendCodePressed, clickEnabled = !state.loading)
189+
ResendCodeText(
190+
onResendCodePressed = onResendCodePressed,
191+
clickEnabled = !state.loading,
192+
timerText = state.remainingTimerText,
193+
)
190194
}
191195
Spacer(modifier = Modifier.weight(1f))
192196
}

app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ import com.wire.android.di.ClientScopeProvider
3131
import com.wire.android.di.KaliumCoreLogic
3232
import com.wire.android.ui.authentication.create.common.CreateAccountFlowType
3333
import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs
34+
import com.wire.android.ui.authentication.login.email.LoginEmailViewModel.Companion.RESEND_TIMER_DELAY
3435
import com.wire.android.ui.common.textfield.textAsFlow
3536
import com.wire.android.ui.navArgs
3637
import com.wire.android.ui.registration.code.CreateAccountCodeResult
3738
import com.wire.android.util.WillNeverOccurError
39+
import com.wire.android.util.ui.CountdownTimer
3840
import com.wire.kalium.logic.CoreLogic
3941
import com.wire.kalium.logic.configuration.server.ServerConfig
4042
import com.wire.kalium.logic.data.user.UserId
@@ -66,6 +68,8 @@ class CreateAccountCodeViewModel @Inject constructor(
6668
val codeTextState: TextFieldState = TextFieldState()
6769
var codeState: CreateAccountCodeViewState by mutableStateOf(CreateAccountCodeViewState(createAccountNavArgs.flowType))
6870

71+
private var resendCodeTimer = CountdownTimer()
72+
6973
init {
7074
viewModelScope.launch {
7175
codeTextState.textAsFlow().collectLatest {
@@ -98,8 +102,13 @@ class CreateAccountCodeViewModel @Inject constructor(
98102
}
99103
}
100104

101-
val result = authScope.registerScope.requestActivationCode(createAccountNavArgs.userRegistrationInfo.email).toCodeError()
102-
codeState = codeState.copy(loading = false, result = result)
105+
val result = authScope.registerScope.requestActivationCode(createAccountNavArgs.userRegistrationInfo.email)
106+
107+
if (result is RequestActivationCodeResult.Success) {
108+
startResendCodeTimer()
109+
}
110+
111+
codeState = codeState.copy(loading = false, result = result.toCodeError())
103112
}
104113
}
105114

@@ -256,4 +265,24 @@ class CreateAccountCodeViewModel @Inject constructor(
256265
is RequestActivationCodeResult.Failure.Generic -> CreateAccountCodeResult.Error.DialogError.GenericError(this.failure)
257266
RequestActivationCodeResult.Success -> CreateAccountCodeResult.None
258267
}
268+
269+
private fun startResendCodeTimer() {
270+
viewModelScope.launch {
271+
resendCodeTimer.start(
272+
seconds = RESEND_TIMER_DELAY,
273+
onUpdate = { timerText ->
274+
updateResendTimer(timerText)
275+
},
276+
onFinish = {
277+
updateResendTimer(null)
278+
}
279+
)
280+
}
281+
}
282+
283+
private fun updateResendTimer(timerText: String?) {
284+
codeState = codeState.copy(
285+
remainingTimerText = timerText?.let { timerText }
286+
)
287+
}
259288
}

app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ data class CreateAccountCodeViewState(
2828
val email: String = "",
2929
val loading: Boolean = false,
3030
val result: CreateAccountCodeResult = CreateAccountCodeResult.None,
31+
val remainingTimerText: String? = null,
3132
)

app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ import androidx.lifecycle.ViewModel
2727
import androidx.lifecycle.viewModelScope
2828
import com.wire.android.BuildConfig
2929
import com.wire.android.datastore.UserDataStore
30+
import com.wire.android.ui.authentication.login.email.LoginEmailViewModel.Companion.RESEND_TIMER_DELAY
3031
import com.wire.android.ui.authentication.verificationcode.VerificationCodeState
3132
import com.wire.android.ui.common.textfield.textAsFlow
33+
import com.wire.android.util.ui.CountdownTimer
3234
import com.wire.kalium.logic.data.auth.verification.VerifiableAction
3335
import com.wire.kalium.logic.feature.auth.verification.RequestSecondFactorVerificationCodeUseCase
3436
import com.wire.kalium.logic.feature.client.GetOrRegisterClientUseCase
@@ -51,6 +53,7 @@ class RegisterDeviceViewModel @Inject constructor(
5153
private val userDataStore: UserDataStore,
5254
private val getSelfUser: GetSelfUserUseCase,
5355
private val requestSecondFactorVerificationCodeUseCase: RequestSecondFactorVerificationCodeUseCase,
56+
private val resendCodeTimer: CountdownTimer,
5457
) : ViewModel() {
5558

5659
val passwordTextState: TextFieldState = TextFieldState()
@@ -198,6 +201,7 @@ class RegisterDeviceViewModel @Inject constructor(
198201
emailUsed = email,
199202
)
200203
updateFlowState(RegisterDeviceFlowState.Default)
204+
startResendCodeTimer()
201205
}
202206

203207
is RequestSecondFactorVerificationCodeUseCase.Result.Failure.Generic -> {
@@ -211,4 +215,24 @@ class RegisterDeviceViewModel @Inject constructor(
211215
private fun updateFlowState(flowState: RegisterDeviceFlowState) {
212216
state = state.copy(flowState = flowState)
213217
}
218+
219+
private fun startResendCodeTimer() {
220+
viewModelScope.launch {
221+
resendCodeTimer.start(
222+
seconds = RESEND_TIMER_DELAY,
223+
onUpdate = { timerText ->
224+
updateResendTimer(timerText)
225+
},
226+
onFinish = {
227+
updateResendTimer(null)
228+
}
229+
)
230+
}
231+
}
232+
233+
private fun updateResendTimer(timerText: String?) {
234+
secondFactorVerificationCodeState = secondFactorVerificationCodeState.copy(
235+
remainingTimerText = timerText?.let { timerText }
236+
)
237+
}
214238
}

app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import com.wire.android.ui.common.textfield.textAsFlow
4141
import com.wire.android.ui.navArgs
4242
import com.wire.android.util.EMPTY
4343
import com.wire.android.util.dispatchers.DispatcherProvider
44+
import com.wire.android.util.ui.CountdownTimer
4445
import com.wire.kalium.logic.CoreLogic
4546
import com.wire.kalium.logic.data.auth.login.ProxyCredentials
4647
import com.wire.kalium.logic.data.auth.verification.VerifiableAction
@@ -66,14 +67,15 @@ import kotlinx.coroutines.launch
6667
import kotlinx.coroutines.withContext
6768
import javax.inject.Inject
6869

69-
@Suppress("LongParameterList", "ComplexMethod")
70+
@Suppress("LongParameterList", "ComplexMethod", "TooManyFunctions")
7071
@HiltViewModel
7172
class LoginEmailViewModel @Inject constructor(
7273
private val addAuthenticatedUser: AddAuthenticatedUserUseCase,
7374
clientScopeProviderFactory: ClientScopeProvider.Factory,
7475
private val savedStateHandle: SavedStateHandle,
7576
userDataStoreProvider: UserDataStoreProvider,
7677
@KaliumCoreLogic coreLogic: CoreLogic,
78+
private val resendCodeTimer: CountdownTimer,
7779
private val dispatchers: DispatcherProvider
7880
) : LoginViewModel(
7981
savedStateHandle,
@@ -346,6 +348,7 @@ class LoginEmailViewModel @Inject constructor(
346348
emailUsed = email,
347349
)
348350
updateEmailFlowState(LoginState.Default)
351+
startResendCodeTimer()
349352
}
350353

351354
is RequestSecondFactorVerificationCodeUseCase.Result.Failure.Generic -> {
@@ -354,6 +357,26 @@ class LoginEmailViewModel @Inject constructor(
354357
}
355358
}
356359

360+
private fun startResendCodeTimer() {
361+
viewModelScope.launch {
362+
resendCodeTimer.start(
363+
seconds = RESEND_TIMER_DELAY,
364+
onUpdate = { timerText ->
365+
updateResendTimer(timerText)
366+
},
367+
onFinish = {
368+
updateResendTimer(null)
369+
}
370+
)
371+
}
372+
}
373+
374+
private fun updateResendTimer(timerText: String?) {
375+
secondFactorVerificationCodeState = secondFactorVerificationCodeState.copy(
376+
remainingTimerText = timerText?.let { timerText }
377+
)
378+
}
379+
357380
fun onCodeVerificationBackPress() {
358381
secondFactorVerificationCodeTextState.clearText()
359382
secondFactorVerificationCodeState = secondFactorVerificationCodeState.copy(
@@ -372,6 +395,7 @@ class LoginEmailViewModel @Inject constructor(
372395

373396
companion object {
374397
const val USER_IDENTIFIER_SAVED_STATE_KEY = "user_identifier"
398+
const val RESEND_TIMER_DELAY = 300L
375399
}
376400
}
377401

app/src/main/kotlin/com/wire/android/ui/authentication/verificationcode/ResendCodeText.kt

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,40 @@ import androidx.compose.ui.res.stringResource
2929
import androidx.compose.ui.text.style.TextAlign
3030
import androidx.compose.ui.text.style.TextDecoration
3131
import com.wire.android.R
32+
import com.wire.android.ui.theme.wireColorScheme
3233
import com.wire.android.ui.theme.WireTheme
3334
import com.wire.android.ui.theme.wireTypography
3435
import com.wire.android.util.ui.PreviewMultipleThemes
3536

3637
@Composable
37-
fun ResendCodeText(onResendCodePressed: () -> Unit, clickEnabled: Boolean, modifier: Modifier = Modifier) {
38+
fun ResendCodeText(
39+
onResendCodePressed: () -> Unit,
40+
clickEnabled: Boolean,
41+
modifier: Modifier = Modifier,
42+
timerText: String? = null,
43+
) {
44+
val enabled = timerText == null && clickEnabled
45+
val label = stringResource(R.string.create_account_code_resend)
3846
Text(
39-
text = stringResource(R.string.create_account_code_resend),
40-
style = MaterialTheme.wireTypography.body02.copy(textDecoration = TextDecoration.Underline),
47+
text = timerText?.let { "$label ($it)" } ?: label,
48+
style = MaterialTheme.wireTypography.body02.copy(
49+
textDecoration = if (enabled) {
50+
TextDecoration.Underline
51+
} else {
52+
TextDecoration.None
53+
},
54+
color = if (enabled) {
55+
MaterialTheme.wireColorScheme.primary
56+
} else {
57+
MaterialTheme.wireColorScheme.onSurface
58+
}
59+
),
4160
textAlign = TextAlign.Center,
4261
modifier = modifier
4362
.clickable(
4463
interactionSource = remember { MutableInteractionSource() },
4564
indication = null,
46-
enabled = clickEnabled,
65+
enabled = enabled,
4766
onClick = onResendCodePressed
4867
)
4968
)

app/src/main/kotlin/com/wire/android/ui/authentication/verificationcode/VerificationCode.kt

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ fun VerificationCode(
5555
onResendCode: () -> Unit,
5656
modifier: Modifier = Modifier,
5757
showLoadingProgress: Boolean = true,
58+
timerText: String? = null,
5859
) {
5960
val focusRequester = remember { FocusRequester() }
6061
val keyboardController = LocalSoftwareKeyboardController.current
@@ -82,13 +83,15 @@ fun VerificationCode(
8283
.animateContentSize(),
8384
) { (isLoading, showLoadingProgress) ->
8485
when {
85-
!isLoading -> ResendCodeText(
86-
onResendCodePressed = onResendCode,
87-
clickEnabled = true,
88-
modifier = Modifier
89-
.defaultMinSize(minHeight = MaterialTheme.wireDimensions.spacing24x)
90-
.wrapContentHeight(align = Alignment.CenterVertically),
91-
)
86+
!isLoading ->
87+
ResendCodeText(
88+
onResendCodePressed = onResendCode,
89+
clickEnabled = true,
90+
timerText = timerText,
91+
modifier = Modifier
92+
.defaultMinSize(minHeight = MaterialTheme.wireDimensions.spacing24x)
93+
.wrapContentHeight(align = Alignment.CenterVertically),
94+
)
9295

9396
isLoading && showLoadingProgress -> WireCircularProgressIndicator(
9497
progressColor = MaterialTheme.wireColorScheme.primary,
@@ -117,3 +120,16 @@ fun PreviewVerificationCode() = WireTheme {
117120
onResendCode = {}
118121
)
119122
}
123+
124+
@PreviewMultipleThemes
125+
@Composable
126+
fun PreviewVerificationCodeTimer() = WireTheme {
127+
VerificationCode(
128+
codeLength = 6,
129+
codeState = TextFieldState(),
130+
isLoading = false,
131+
isCurrentCodeInvalid = false,
132+
onResendCode = {},
133+
timerText = "00:30",
134+
)
135+
}

app/src/main/kotlin/com/wire/android/ui/authentication/verificationcode/VerificationCodeScreenContent.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ fun VerificationCodeScreenContent(
4848
isLoading: Boolean,
4949
onCodeResend: () -> Unit,
5050
onBackPressed: () -> Unit,
51+
modifier: Modifier = Modifier,
5152
) {
5253
BackHandler { onBackPressed() }
5354
WireScaffold(
55+
modifier = modifier,
5456
topBar = {
5557
WireCenterAlignedTopAppBar(
5658
elevation = dimensions().spacing0x,
@@ -78,7 +80,7 @@ private fun MainContent(
7880
codeState: VerificationCodeState,
7981
isLoading: Boolean,
8082
onResendCode: () -> Unit,
81-
modifier: Modifier = Modifier
83+
modifier: Modifier = Modifier,
8284
) = Column(
8385
horizontalAlignment = Alignment.CenterHorizontally,
8486
verticalArrangement = Arrangement.Center,
@@ -104,6 +106,7 @@ private fun MainContent(
104106
isLoading = isLoading,
105107
isCurrentCodeInvalid = codeState.isCurrentCodeInvalid,
106108
onResendCode = onResendCode,
109+
timerText = codeState.remainingTimerText,
107110
)
108111
Spacer(
109112
modifier = Modifier
@@ -127,3 +130,20 @@ internal fun VerificationCodeScreenPreview() = WireTheme {
127130
onBackPressed = {},
128131
)
129132
}
133+
134+
@PreviewMultipleThemes
135+
@Composable
136+
internal fun VerificationCodeScreenPreviewTimer() = WireTheme {
137+
VerificationCodeScreenContent(
138+
verificationCodeTextState = TextFieldState(),
139+
verificationCodeState = VerificationCodeState(
140+
codeLength = 6,
141+
isCurrentCodeInvalid = false,
142+
emailUsed = "",
143+
remainingTimerText = "04:30"
144+
),
145+
isLoading = false,
146+
onCodeResend = {},
147+
onBackPressed = {},
148+
)
149+
}

app/src/main/kotlin/com/wire/android/ui/authentication/verificationcode/VerificationCodeState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ data class VerificationCodeState(
2323
val emailUsed: String = "",
2424
val isCodeInputNecessary: Boolean = false,
2525
val isCurrentCodeInvalid: Boolean = false,
26+
val remainingTimerText: String? = null,
2627
) {
2728
companion object {
2829
const val DEFAULT_VERIFICATION_CODE_LENGTH = 6
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2025 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*/
18+
package com.wire.android.util.ui
19+
20+
import android.text.format.DateUtils.formatElapsedTime
21+
import kotlinx.coroutines.Job
22+
import kotlinx.coroutines.coroutineScope
23+
import kotlinx.coroutines.delay
24+
import kotlinx.coroutines.isActive
25+
import kotlinx.coroutines.launch
26+
import javax.inject.Inject
27+
import kotlin.time.Duration.Companion.seconds
28+
29+
class CountdownTimer @Inject constructor() {
30+
private var timerJob: Job? = null
31+
32+
suspend fun start(seconds: Long, onUpdate: (String) -> Unit, onFinish: () -> Unit) {
33+
timerJob?.cancel()
34+
timerJob = coroutineScope {
35+
launch {
36+
var countdown = seconds
37+
while (countdown > 0 && isActive) {
38+
onUpdate(formatElapsedTime(countdown--))
39+
delay(1.seconds)
40+
}
41+
onFinish()
42+
}
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)