commit
52be4071a4
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<manifest package="ru.touchin.code_confirm"/>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.code_confirm
|
||||
|
||||
abstract class BaseCodeResponse(
|
||||
open val codeLifetime: Int,
|
||||
open val codeId: String? = null
|
||||
)
|
||||
|
|
@ -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))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue