Skip to content

Commit

Permalink
introduce AbstractBackupComponent to make proper synchronization in…
Browse files Browse the repository at this point in the history
… all places
  • Loading branch information
avan1235 committed Nov 7, 2023
1 parent 8bd1bd3 commit 3fa2093
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 132 deletions.
24 changes: 21 additions & 3 deletions shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/OpenOtpApp.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
package ml.dev.kotlin.openotp

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.jetbrains.stack.Children
import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.builtins.ListSerializer
import ml.dev.kotlin.openotp.component.LinkedAccountsSyncState.NothingToSync
import ml.dev.kotlin.openotp.component.OpenOtpAppComponent
import ml.dev.kotlin.openotp.component.OpenOtpAppComponent.Child
import ml.dev.kotlin.openotp.component.UserLinkedAccountsModel
import ml.dev.kotlin.openotp.component.UserPreferencesModel
import ml.dev.kotlin.openotp.otp.OtpData
import ml.dev.kotlin.openotp.ui.screen.*
import ml.dev.kotlin.openotp.ui.theme.OpenOtpTheme
import ml.dev.kotlin.openotp.util.*
import ml.dev.kotlin.openotp.util.BindBiometryAuthenticatorEffect
import ml.dev.kotlin.openotp.util.BiometryAuthenticator
import ml.dev.kotlin.openotp.util.OnceLaunchedEffect
import ml.dev.kotlin.openotp.util.StateFlowSettings
import org.koin.compose.koinInject
import org.koin.core.context.startKoin
import org.koin.core.module.Module
Expand Down Expand Up @@ -65,6 +74,8 @@ internal fun OpenOtpApp(component: OpenOtpAppComponent) {

internal val USER_OTP_CODE_DATA_MODULE_QUALIFIER: StringQualifier = named("userOtpCodeDataModule")

internal val LINKED_ACCOUNTS_SYNC_STATE_MODULE_QUALIFIER: StringQualifier = named("linkedAccountsSyncStateModule")

internal val USER_PREFERENCES_MODULE_QUALIFIER: StringQualifier = named("userPreferencesModule")

internal val USER_LINKED_ACCOUNTS_MODULE_QUALIFIER: StringQualifier = named("userLinkedAccountsModule")
Expand All @@ -75,6 +86,7 @@ internal fun initOpenOtpKoin(appDeclaration: KoinAppDeclaration = {}) {
appDeclaration()
modules(module {
userOtpCodeDataModule()
linkedAccountsSyncStateModule()
userPreferencesModule()
userLinkedAccountsModule()
snackbarHostStateModule()
Expand All @@ -93,6 +105,12 @@ private fun Module.userOtpCodeDataModule() {
}
}

private fun Module.linkedAccountsSyncStateModule() {
single(LINKED_ACCOUNTS_SYNC_STATE_MODULE_QUALIFIER) {
MutableStateFlow(NothingToSync)
}
}

private fun Module.userPreferencesModule() {
single(USER_PREFERENCES_MODULE_QUALIFIER) {
StateFlowSettings(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package ml.dev.kotlin.openotp.component

import com.arkivanov.decompose.ComponentContext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import ml.dev.kotlin.openotp.LINKED_ACCOUNTS_SYNC_STATE_MODULE_QUALIFIER
import ml.dev.kotlin.openotp.USER_LINKED_ACCOUNTS_MODULE_QUALIFIER
import ml.dev.kotlin.openotp.USER_OTP_CODE_DATA_MODULE_QUALIFIER
import ml.dev.kotlin.openotp.backup.*
import ml.dev.kotlin.openotp.backup.SerializedStoredOtpCodeDataBackup.Companion.toSerializedBackup
import ml.dev.kotlin.openotp.otp.StoredOtpCodeData
import ml.dev.kotlin.openotp.shared.OpenOtpResources
import ml.dev.kotlin.openotp.util.StateFlowSettings
import ml.dev.kotlin.openotp.util.unit
import org.koin.core.component.get

abstract class AbstractBackupComponent(
componentContext: ComponentContext,
) : AbstractComponent(componentContext) {

protected val _userOtpCodeData: StateFlowSettings<StoredOtpCodeData> =
get(USER_OTP_CODE_DATA_MODULE_QUALIFIER)

protected val _linkedAccountsSyncState: MutableStateFlow<LinkedAccountsSyncState> =
get(LINKED_ACCOUNTS_SYNC_STATE_MODULE_QUALIFIER)

protected val _userLinkedAccounts: StateFlowSettings<UserLinkedAccountsModel> =
get(USER_LINKED_ACCOUNTS_MODULE_QUALIFIER)

protected fun refreshBackup(download: Boolean = false) {
val linkedAccounts = _userLinkedAccounts.stateFlow.value
val syncAccountsCount = UserLinkedAccountType.entries.count { it.isLinked(linkedAccounts) }

if (syncAccountsCount == 0) {
_linkedAccountsSyncState.value = LinkedAccountsSyncState.NothingToSync
return
}
_linkedAccountsSyncState.value = LinkedAccountsSyncState.Refreshing

scope.launch {
var success = true
try {
if (download) {
downloadBackup(linkedAccounts)
}
val backup = _userOtpCodeData.stateFlow.value.toSerializedBackup()
uploadBackup(linkedAccounts, backup)
} catch (_: Exception) {
success = false
} finally {
if (!success) _linkedAccountsSyncState.value = LinkedAccountsSyncState.NotSynced
}
}
}

private suspend fun uploadBackup(
linkedAccounts: UserLinkedAccountsModel,
backup: SerializedStoredOtpCodeDataBackup,
) {
supervisorScope {
val jobs = mutableListOf<Deferred<Boolean>>()
for (accountType in UserLinkedAccountType.entries) {
val service = accountType.createAuthenticatedService(linkedAccounts) ?: continue
jobs += async { service.uploadBackup(backup) }
}
val results = jobs.awaitAll()
when {
results.isEmpty() -> Unit

results.all { it } -> {
_linkedAccountsSyncState.value = LinkedAccountsSyncState.Synced
toast(stringResource(OpenOtpResources.strings.synced_all))
}

results.any { it } -> {
_linkedAccountsSyncState.value = LinkedAccountsSyncState.Synced
toast(stringResource(OpenOtpResources.strings.failed_some))
}

else -> {
_linkedAccountsSyncState.value = LinkedAccountsSyncState.NotSynced
toast(stringResource(OpenOtpResources.strings.failed_all))
}
}
}
}

private suspend fun downloadBackup(
linkedAccounts: UserLinkedAccountsModel,
) {
val currentCodeData = _userOtpCodeData.stateFlow.value
val updatedCodeData = supervisorScope {
val jobs = mutableListOf<Deferred<StoredOtpCodeDataBackup?>>()
for (accountType in UserLinkedAccountType.entries) {
val service = accountType.createAuthenticatedService(linkedAccounts) ?: continue
jobs += async { service.downloadBackup() }
}
val downloadedData = jobs.awaitAll().filterNotNull()
downloadedData.merge(current = currentCodeData)
}
_userOtpCodeData.updateInScope { updatedCodeData }.unit()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.value.Value
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import ml.dev.kotlin.openotp.USER_OTP_CODE_DATA_MODULE_QUALIFIER
import ml.dev.kotlin.openotp.otp.*
import ml.dev.kotlin.openotp.shared.OpenOtpResources
import ml.dev.kotlin.openotp.util.StateFlowSettings
import ml.dev.kotlin.openotp.util.isValidBase32Secret
import ml.dev.kotlin.openotp.util.unit
import org.koin.core.component.get

interface AddOtpProviderComponent {

Expand Down Expand Up @@ -49,9 +46,7 @@ abstract class AddOtpProviderComponentImpl(
componentContext: ComponentContext,
private val navigateOnSaveClicked: () -> Unit,
private val navigateOnCancelClicked: () -> Unit,
) : AbstractComponent(componentContext), AddOtpProviderComponent {

protected val secureStorage: StateFlowSettings<StoredOtpCodeData> = get(USER_OTP_CODE_DATA_MODULE_QUALIFIER)
) : AbstractBackupComponent(componentContext), AddOtpProviderComponent {

protected fun notifyInvalid(fieldName: String) {
toast(message = stringResource(OpenOtpResources.strings.invalid_field_name_provided_formatted, fieldName))
Expand Down Expand Up @@ -137,7 +132,7 @@ class AddTotpProviderComponentImpl(

val config = TotpConfig(period, digits, algorithm)
val codeData = TotpData(issuer, accountName, secret, config)
secureStorage
_userOtpCodeData
.updateInScope { it + codeData }
.invokeOnCompletion {
super.onSaveClicked()
Expand Down Expand Up @@ -229,7 +224,7 @@ class AddHotpProviderComponentImpl(

val config = HotpConfig(digits, algorithm)
val codeData = HotpData(issuer, accountName, secret, counter, config)
secureStorage
_userOtpCodeData
.updateInScope { it + codeData }
.invokeOnCompletion {
super.onSaveClicked()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import com.arkivanov.decompose.value.Value
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ml.dev.kotlin.openotp.USER_LINKED_ACCOUNTS_MODULE_QUALIFIER
import ml.dev.kotlin.openotp.backup.OAuth2AccountService
import ml.dev.kotlin.openotp.util.StateFlowSettings
import org.koin.core.component.get

interface LinkAccountComponent {

Expand All @@ -28,10 +25,7 @@ class LinkAccountComponentImpl(
private val accountType: UserLinkedAccountType,
componentContext: ComponentContext,
private val navigateOnCancel: () -> Unit,
) : AbstractComponent(componentContext), LinkAccountComponent {

private val userLinkedAccounts: StateFlowSettings<UserLinkedAccountsModel> =
get(USER_LINKED_ACCOUNTS_MODULE_QUALIFIER)
) : AbstractBackupComponent(componentContext), LinkAccountComponent {

private val requestedPermissions: MutableStateFlow<OAuth2AccountService.RequestedPermissions?> =
MutableStateFlow(null)
Expand Down Expand Up @@ -70,7 +64,7 @@ class LinkAccountComponentImpl(

requestedPermissions.authenticateUser(code.value)
.onSuccess { authenticated ->
userLinkedAccounts
_userLinkedAccounts
.updateInScope(this, authenticated::updateUserLinkedAccounts)
.invokeOnCompletion {
_isLoadingAppPermissions.update { false }
Expand Down
Loading

0 comments on commit 3fa2093

Please sign in to comment.