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..d135f88
--- /dev/null
+++ b/code-confirm/build.gradle
@@ -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)
+ }
+ }
+ }
+}
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 @@
+
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..3a916d8
--- /dev/null
+++ b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmState.kt
@@ -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 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..1d78e32
--- /dev/null
+++ b/code-confirm/src/main/java/ru/touchin/code_confirm/BaseCodeConfirmViewModel.kt
@@ -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(
+ initialState: State,
+ savedStateHandle: SavedStateHandle
+) : MviViewModel(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()
+ 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()
+ }
+
+}
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..9478076
--- /dev/null
+++ b/code-confirm/src/main/java/ru/touchin/code_confirm/LifeTimer.kt
@@ -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))
+
+ }
+
+}