From 72c9b70d329e46bb5f6385a28504e784a9e3ee87 Mon Sep 17 00:00:00 2001 From: AnastasiyaK97 Date: Mon, 12 Sep 2022 16:49:42 +0300 Subject: [PATCH 1/4] add code-confirm module --- code-confirm/.gitignore | 1 + code-confirm/build.gradle | 7 +++++++ code-confirm/src/main/AndroidManifest.xml | 1 + 3 files changed, 9 insertions(+) create mode 100644 code-confirm/.gitignore create mode 100644 code-confirm/build.gradle create mode 100644 code-confirm/src/main/AndroidManifest.xml diff --git a/code-confirm/.gitignore b/code-confirm/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/code-confirm/.gitignore @@ -0,0 +1 @@ +/build diff --git a/code-confirm/build.gradle b/code-confirm/build.gradle new file mode 100644 index 0000000..da4f571 --- /dev/null +++ b/code-confirm/build.gradle @@ -0,0 +1,7 @@ +apply from: "../android-configs/lib-config.gradle" + +dependencies { + constraints { + + } +} diff --git a/code-confirm/src/main/AndroidManifest.xml b/code-confirm/src/main/AndroidManifest.xml new file mode 100644 index 0000000..749fd3b --- /dev/null +++ b/code-confirm/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + From c50c67e28fe0a297f4a53a296301e1a4686f6f31 Mon Sep 17 00:00:00 2001 From: AnastasiyaK97 Date: Wed, 14 Sep 2022 16:51:14 +0300 Subject: [PATCH 2/4] Add "code confirm" base realization --- code-confirm/build.gradle | 19 ++- .../code_confirm/BaseCodeConfirmAction.kt | 22 +++ .../code_confirm/BaseCodeConfirmState.kt | 14 ++ .../code_confirm/BaseCodeConfirmViewModel.kt | 138 ++++++++++++++++++ .../touchin/code_confirm/BaseCodeResponse.kt | 6 + .../java/ru/touchin/code_confirm/LifeTimer.kt | 31 ++++ 6 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmAction.kt create mode 100644 code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt create mode 100644 code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmViewModel.kt create mode 100644 code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeResponse.kt create mode 100644 code-confirm/src/main/java/ru/touchin/code_confirm/LifeTimer.kt diff --git a/code-confirm/build.gradle b/code-confirm/build.gradle index da4f571..d135f88 100644 --- a/code-confirm/build.gradle +++ b/code-confirm/build.gradle @@ -1,7 +1,24 @@ apply from: "../android-configs/lib-config.gradle" dependencies { - constraints { + implementation project(":mvi-arch") + implementation project(":lifecycle") + implementation "androidx.lifecycle:lifecycle-extensions" + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx") + + def lifecycleVersion = "2.2.0" + + constraints { + implementation("androidx.lifecycle:lifecycle-extensions") { + version { + require '2.1.0' + } + } + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx") { + version { + require(lifecycleVersion) + } + } } } diff --git a/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmAction.kt b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmAction.kt new file mode 100644 index 0000000..745bef2 --- /dev/null +++ b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmAction.kt @@ -0,0 +1,22 @@ +package ru.touchin.code_confirm + +/** + * [CodeConfirmAction] is interface for the action that will call + * the confirmation request with entered code + */ +interface CodeConfirmAction + +/** + * [UpdatedCodeInputAction] is interface for the action, that should be called + * after each update of codeInput + * @param code Updated string with code from codeInput + */ +interface UpdatedCodeInputAction { + val code: String? +} + +/** + * [GetRefreshCodeAction] is interface for the action that will call + * the request of a repeat code after it's expired + */ +interface GetRefreshCodeAction diff --git a/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt new file mode 100644 index 0000000..983ec13 --- /dev/null +++ b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt @@ -0,0 +1,14 @@ +package ru.touchin.code_confirm + +import ru.touchin.roboswag.mvi_arch.marker.ViewState + +abstract class BaseCodeConfirmState( + open var codeLifetime: String, + open var isLoadingState: Boolean, + open var isWrongCode: Boolean, + open var isExpired: Boolean, + open var isRefreshCodeLoading: Boolean = false +) : ViewState { + + abstract fun copyWith(updateBlock: T.() -> Unit): T +} diff --git a/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmViewModel.kt b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmViewModel.kt new file mode 100644 index 0000000..0a1579f --- /dev/null +++ b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmViewModel.kt @@ -0,0 +1,138 @@ +package ru.touchin.code_confirm + +import android.os.CountDownTimer +import android.os.Parcelable +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import ru.touchin.code_confirm.LifeTimer.Companion.getFormattedCodeLifetimeString +import ru.touchin.lifecycle.extensions.toImmutable +import ru.touchin.lifecycle.livedata.SingleLiveEvent +import ru.touchin.roboswag.mvi_arch.core.MviViewModel +import ru.touchin.roboswag.mvi_arch.marker.ViewAction + +@SuppressWarnings("detekt.TooGenericExceptionCaught") +abstract class BaseCodeConfirmViewModel( + initialState: State, + savedStateHandle: SavedStateHandle +) : MviViewModel(initialState, savedStateHandle) { + + /** [requireCodeId] uses for auto-filling */ + protected open var requireCodeId: String? = null + + /** [needSendCode] is flag for preventing sending confirmation code everytime + * when view is recreated and codeInput is filled */ + private var needSendCode: Boolean = true + + private var timer: CountDownTimer? = null + + private var currentConfirmationCode: String? = null + + private val _updateCodeEvent = SingleLiveEvent() + val updateCodeEvent = _updateCodeEvent.toImmutable() + + init { + _state.value = currentState.copyWith { + codeLifetime = getFormattedCodeLifetimeString(getTimerDuration().toLong()) + } + + startTimer(seconds = getTimerDuration()) + } + + protected abstract fun getTimerDuration(): Int + protected abstract suspend fun requestNewCode(): BaseCodeResponse + protected abstract suspend fun requestCodeConfirmation(code: String) + + protected open fun onRefreshCodeRequestError(e: Throwable) {} + protected open fun onCodeConfirmationError(e: Throwable) {} + protected open fun onSuccessCodeConfirmation(code: String) {} + + override fun dispatchAction(action: Action) { + super.dispatchAction(action) + when (action) { + is CodeConfirmAction -> { + if (needSendCode) confirmCode() + } + is GetRefreshCodeAction -> { + getRefreshCode() + } + is UpdatedCodeInputAction -> { + val confirmationCodeChanged = currentConfirmationCode != action.code + + _state.value = currentState.copyWith { + isWrongCode = isWrongCode && !confirmationCodeChanged + } + needSendCode = confirmationCodeChanged + currentConfirmationCode = action.code + } + } + } + + protected open fun startTimer(seconds: Int) { + timer?.cancel() + timer = LifeTimer( + seconds = seconds, + tickAction = { millis -> + _state.value = currentState.copyWith { + codeLifetime = getFormattedCodeLifetimeString(millis) + isExpired = false + } + }, + finishAction = { + _state.value = currentState.copyWith { + isExpired = true + } + } + ) + timer?.start() + } + + protected open fun getRefreshCode() { + viewModelScope.launch { + try { + _state.value = currentState.copyWith { + isRefreshCodeLoading = true + isWrongCode = false + } + val confirmationData = requestNewCode() + requireCodeId = confirmationData.codeId + + startTimer(seconds = confirmationData.codeLifetime) + } catch (throwable: Throwable) { + needSendCode = false + onRefreshCodeRequestError(throwable) + } finally { + _state.value = currentState.copyWith { isRefreshCodeLoading = false } + } + } + } + + protected open fun confirmCode() { + currentConfirmationCode?.let { code -> + _state.value = currentState.copyWith { isLoadingState = true } + viewModelScope.launch { + try { + requestCodeConfirmation(code) + onSuccessCodeConfirmation(code) + } catch (throwable: Throwable) { + needSendCode = false + onCodeConfirmationError(throwable) + } finally { + _state.value = currentState.copyWith { isLoadingState = false } + } + } + } + } + + protected open fun autofillCode(code: String, codeId: String? = null) { + if (codeId == requireCodeId) { + _updateCodeEvent.setValue(code) + } + } + + override fun onCleared() { + super.onCleared() + timer?.cancel() + } + +} diff --git a/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeResponse.kt b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeResponse.kt new file mode 100644 index 0000000..a300d4e --- /dev/null +++ b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeResponse.kt @@ -0,0 +1,6 @@ +package ru.touchin.code_confirm + +abstract class BaseCodeResponse( + open val codeLifetime: Int, + open val codeId: String? = null +) diff --git a/code-confirm/src/main/java/ru/touchin/code_confirm/LifeTimer.kt b/code-confirm/src/main/java/ru/touchin/code_confirm/LifeTimer.kt new file mode 100644 index 0000000..b484a39 --- /dev/null +++ b/code-confirm/src/main/java/ru/touchin/code_confirm/LifeTimer.kt @@ -0,0 +1,31 @@ +package ru.touchin.code_confirm + +import android.os.CountDownTimer + +/** [LifeTimer] is extends [CountDownTimer] for countdown in seconds and lifetime text formatting + * @param seconds Lifetime of timer in seconds + * @param tickAction Action will be called on regular interval + * @param finishAction Action will be called on finish */ +class LifeTimer( + seconds: Int, + private val tickAction: (Long) -> Unit, + private val finishAction: () -> Unit +) : CountDownTimer(seconds.toLong() * 1000, 1000) { + + override fun onTick(millisUntilFinished: Long) { + tickAction.invoke(millisUntilFinished / 1000) + } + + override fun onFinish() { + finishAction.invoke() + } + + companion object { + fun getFormattedCodeLifetimeString(secondsUntilFinished: Long): String { + val seconds = (secondsUntilFinished % 60).let { if (it < 10) "0$it" else "$it" } + val minutes = (secondsUntilFinished / 60).let { if (it < 10) "0$it" else "$it" } + return "$minutes:$seconds" + } + } + +} From 3057797b237c4e2f1bd77b16ac7c5252e4d921c9 Mon Sep 17 00:00:00 2001 From: AnastasiyaK97 Date: Mon, 19 Sep 2022 15:16:08 +0300 Subject: [PATCH 3/4] fix by PR comments --- .../touchin/code_confirm/BaseCodeConfirmState.kt | 3 +++ .../main/java/ru/touchin/code_confirm/LifeTimer.kt | 14 +++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt index 983ec13..32024dd 100644 --- a/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt +++ b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt @@ -10,5 +10,8 @@ abstract class BaseCodeConfirmState( open var isRefreshCodeLoading: Boolean = false ) : ViewState { + val canRequestNewCode: Boolean + get() = isExpired && !isRefreshCodeLoading + abstract fun copyWith(updateBlock: T.() -> Unit): T } diff --git a/code-confirm/src/main/java/ru/touchin/code_confirm/LifeTimer.kt b/code-confirm/src/main/java/ru/touchin/code_confirm/LifeTimer.kt index b484a39..9478076 100644 --- a/code-confirm/src/main/java/ru/touchin/code_confirm/LifeTimer.kt +++ b/code-confirm/src/main/java/ru/touchin/code_confirm/LifeTimer.kt @@ -1,6 +1,9 @@ package ru.touchin.code_confirm import android.os.CountDownTimer +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.Date /** [LifeTimer] is extends [CountDownTimer] for countdown in seconds and lifetime text formatting * @param seconds Lifetime of timer in seconds @@ -21,11 +24,12 @@ class LifeTimer( } companion object { - fun getFormattedCodeLifetimeString(secondsUntilFinished: Long): String { - val seconds = (secondsUntilFinished % 60).let { if (it < 10) "0$it" else "$it" } - val minutes = (secondsUntilFinished / 60).let { if (it < 10) "0$it" else "$it" } - return "$minutes:$seconds" - } + + private val formatter = SimpleDateFormat("mm:ss", Locale.ROOT) + + fun getFormattedCodeLifetimeString(secondsUntilFinished: Long): String = + formatter.format(Date(secondsUntilFinished * 1000L)) + } } From ce3c58ee90374eeb1ce57597918407d19266f2e9 Mon Sep 17 00:00:00 2001 From: Alexei Ganin <74057882+GaninAlexei@users.noreply.github.com> Date: Fri, 16 Dec 2022 11:29:42 +0300 Subject: [PATCH 4/4] fix issues from Ekaterina Kacharova --- .../ru/touchin/code_confirm/BaseCodeConfirmState.kt | 3 ++- .../touchin/code_confirm/BaseCodeConfirmViewModel.kt | 12 ++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt index 32024dd..3a916d8 100644 --- a/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt +++ b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt @@ -7,7 +7,8 @@ abstract class BaseCodeConfirmState( open var isLoadingState: Boolean, open var isWrongCode: Boolean, open var isExpired: Boolean, - open var isRefreshCodeLoading: Boolean = false + open var isRefreshCodeLoading: Boolean = false, + open var needSendCode: Boolean = true ) : ViewState { val canRequestNewCode: Boolean diff --git a/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmViewModel.kt b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmViewModel.kt index 0a1579f..1d78e32 100644 --- a/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmViewModel.kt +++ b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmViewModel.kt @@ -20,10 +20,6 @@ abstract class BaseCodeConfirmViewModel { - if (needSendCode) confirmCode() + if (currentState.needSendCode) confirmCode() } is GetRefreshCodeAction -> { getRefreshCode() @@ -61,8 +57,8 @@ abstract class BaseCodeConfirmViewModel