Merge pull request #267 from TouchInstinct/code_confirm

Code confirm
This commit is contained in:
Ganin Alexei 2022-12-19 19:23:55 +03:00 committed by GitHub
commit 52be4071a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 241 additions and 0 deletions

1
code-confirm/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

24
code-confirm/build.gradle Normal file
View File

@ -0,0 +1,24 @@
apply from: "../android-configs/lib-config.gradle"
dependencies {
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)
}
}
}
}

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.code_confirm"/>

View File

@ -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

View File

@ -0,0 +1,18 @@
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,
open var needSendCode: Boolean = true
) : ViewState {
val canRequestNewCode: Boolean
get() = isExpired && !isRefreshCodeLoading
abstract fun <T : BaseCodeConfirmState> copyWith(updateBlock: T.() -> Unit): T
}

View File

@ -0,0 +1,134 @@
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<NavArgs : Parcelable, Action : ViewAction, State : BaseCodeConfirmState>(
initialState: State,
savedStateHandle: SavedStateHandle
) : MviViewModel<NavArgs, Action, State>(initialState, savedStateHandle) {
/** [requireCodeId] uses for auto-filling */
protected open var requireCodeId: String? = null
private var timer: CountDownTimer? = null
private var currentConfirmationCode: String? = null
private val _updateCodeEvent = SingleLiveEvent<String>()
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 (currentState.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) {
_state.value = currentState.copyWith { 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) {
_state.value = currentState.copyWith { 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()
}
}

View File

@ -0,0 +1,6 @@
package ru.touchin.code_confirm
abstract class BaseCodeResponse(
open val codeLifetime: Int,
open val codeId: String? = null
)

View File

@ -0,0 +1,35 @@
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
* @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 {
private val formatter = SimpleDateFormat("mm:ss", Locale.ROOT)
fun getFormattedCodeLifetimeString(secondsUntilFinished: Long): String =
formatter.format(Date(secondsUntilFinished * 1000L))
}
}