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