mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 21:57:56 +00:00
Initial commit: Pezkuwi Wallet Android
Complete rebrand of Nova Wallet for Pezkuwichain ecosystem. ## Features - Full Pezkuwichain support (HEZ & PEZ tokens) - Polkadot ecosystem compatibility - Staking, Governance, DeFi, NFTs - XCM cross-chain transfers - Hardware wallet support (Ledger, Polkadot Vault) - WalletConnect v2 - Push notifications ## Languages - English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese Based on Nova Wallet by Novasama Technologies GmbH © Dijital Kurdistan Tech Institute 2026
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.di
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.CloudBackupService
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.mixin.CloudBackupChangingWarningMixinFactory
|
||||
|
||||
interface CloudBackupFeatureApi {
|
||||
|
||||
val cloudBackupService: CloudBackupService
|
||||
|
||||
val cloudBackupChangingWarningMixinFactory: CloudBackupChangingWarningMixinFactory
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain
|
||||
|
||||
import io.novafoundation.nova.common.utils.flatMap
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.EncryptedCloudBackup
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.PreCreateValidationStatus
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.WriteBackupRequest
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.DeleteBackupError
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.FetchBackupError
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.InvalidBackupPasswordError
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.PasswordNotSaved
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.WriteBackupError
|
||||
|
||||
/**
|
||||
* Manages cloud backup storage, serialization and encryption
|
||||
*
|
||||
* The storing pipeline is the following:
|
||||
*
|
||||
* Backup -> serialize private data -> encrypt private data -> serialize public data + encrypted private
|
||||
*
|
||||
* This allows to access public data without accessing password
|
||||
*/
|
||||
interface CloudBackupService {
|
||||
|
||||
/**
|
||||
* Current user preferences, state known for cloud backup related functionality
|
||||
*/
|
||||
val session: CloudBackupSession
|
||||
|
||||
/**
|
||||
* Checks conditions for creating initial backup
|
||||
*/
|
||||
suspend fun validateCanCreateBackup(): PreCreateValidationStatus
|
||||
|
||||
/**
|
||||
* Writes a backup to the cloud, overwriting already existing one
|
||||
*
|
||||
* @throws WriteBackupError
|
||||
*/
|
||||
suspend fun writeBackupToCloud(request: WriteBackupRequest): Result<Unit>
|
||||
|
||||
/**
|
||||
* Check if backup file exists in the cloud
|
||||
*/
|
||||
suspend fun isCloudBackupExist(): Result<Boolean>
|
||||
|
||||
/**
|
||||
* @throws FetchBackupError
|
||||
*/
|
||||
suspend fun fetchBackup(): Result<EncryptedCloudBackup>
|
||||
|
||||
/**
|
||||
* @throws DeleteBackupError
|
||||
*/
|
||||
suspend fun deleteBackup(): Result<Unit>
|
||||
|
||||
suspend fun signInToCloud(): Result<Unit>
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FetchBackupError
|
||||
* @throws InvalidBackupPasswordError
|
||||
*/
|
||||
suspend fun CloudBackupService.fetchAndDecryptExistingBackup(password: String): Result<CloudBackup> {
|
||||
return fetchBackup().flatMap { it.decrypt(password) }
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PasswordNotSaved
|
||||
* @throws FetchBackupError
|
||||
* @throws InvalidBackupPasswordError
|
||||
*/
|
||||
suspend fun CloudBackupService.fetchAndDecryptExistingBackupWithSavedPassword(): Result<CloudBackup> {
|
||||
return fetchBackup().flatMap { encryptedBackup ->
|
||||
session.getSavedPassword().flatMap { password ->
|
||||
encryptedBackup.decrypt(password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun CloudBackupService.writeCloudBackupWithSavedPassword(cloudBackup: CloudBackup): Result<Unit> {
|
||||
return session.getSavedPassword()
|
||||
.flatMap { password ->
|
||||
writeBackupToCloud(WriteBackupRequest(cloudBackup, password))
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.PasswordNotSaved
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.Date
|
||||
|
||||
interface CloudBackupSession {
|
||||
|
||||
/**
|
||||
* Check if user enabled sync with cloud backup in the current application instance
|
||||
* Enabling this means that the app should write changes of local state to backup on addition, modification or delegation of accounts
|
||||
*/
|
||||
suspend fun isSyncWithCloudEnabled(): Boolean
|
||||
|
||||
/**
|
||||
* @see isSyncWithCloudEnabled
|
||||
*/
|
||||
suspend fun setSyncingBackupEnabled(enable: Boolean)
|
||||
|
||||
/**
|
||||
* Observe the time of the latest backup sync
|
||||
*/
|
||||
fun lastSyncedTimeFlow(): Flow<Date?>
|
||||
|
||||
/**
|
||||
* @see lastSyncedTimeFlow
|
||||
*/
|
||||
suspend fun setLastSyncedTime(date: Date)
|
||||
|
||||
/**
|
||||
* @throws PasswordNotSaved
|
||||
*/
|
||||
suspend fun getSavedPassword(): Result<String>
|
||||
|
||||
suspend fun setSavedPassword(password: String)
|
||||
|
||||
fun cloudBackupWasInitialized(): Boolean
|
||||
|
||||
fun setBackupWasInitialized()
|
||||
}
|
||||
|
||||
suspend fun CloudBackupSession.setLastSyncedTimeAsNow() {
|
||||
setLastSyncedTime(Date())
|
||||
}
|
||||
|
||||
suspend fun CloudBackupSession.initEnabledBackup(password: String) {
|
||||
setBackupWasInitialized()
|
||||
setSyncingBackupEnabled(true)
|
||||
setLastSyncedTimeAsNow()
|
||||
setSavedPassword(password)
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.Identifiable
|
||||
import io.novafoundation.nova.core.model.CryptoType
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
data class CloudBackup(
|
||||
val publicData: PublicData,
|
||||
val privateData: PrivateData
|
||||
) {
|
||||
|
||||
data class PublicData(
|
||||
val modifiedAt: Long,
|
||||
val wallets: List<WalletPublicInfo>
|
||||
)
|
||||
|
||||
data class WalletPublicInfo(
|
||||
val walletId: String,
|
||||
val substratePublicKey: ByteArray?,
|
||||
val substrateAccountId: ByteArray?,
|
||||
val substrateCryptoType: CryptoType?,
|
||||
val ethereumAddress: ByteArray?,
|
||||
val ethereumPublicKey: ByteArray?,
|
||||
val name: String,
|
||||
val type: Type,
|
||||
val chainAccounts: Set<ChainAccountInfo>
|
||||
) : Identifiable {
|
||||
|
||||
override val identifier: String = walletId
|
||||
|
||||
enum class Type {
|
||||
SECRETS, WATCH_ONLY, PARITY_SIGNER, LEDGER, LEDGER_GENERIC, POLKADOT_VAULT
|
||||
}
|
||||
|
||||
data class ChainAccountInfo(
|
||||
val chainId: ChainId,
|
||||
val publicKey: ByteArray?,
|
||||
val accountId: ByteArray,
|
||||
val cryptoType: ChainAccountCryptoType?,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is ChainAccountInfo) return false
|
||||
|
||||
return chainId == other.chainId &&
|
||||
publicKey.contentEquals(other.publicKey) &&
|
||||
accountId.contentEquals(other.accountId) &&
|
||||
cryptoType == other.cryptoType
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = chainId.hashCode()
|
||||
result = 31 * result + (publicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + accountId.contentHashCode()
|
||||
result = 31 * result + (cryptoType?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
enum class ChainAccountCryptoType {
|
||||
SR25519, ED25519, ECDSA, ETHEREUM
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is WalletPublicInfo) return false
|
||||
|
||||
return walletId == other.walletId &&
|
||||
substratePublicKey.contentEquals(other.substratePublicKey) &&
|
||||
substrateAccountId.contentEquals(other.substrateAccountId) &&
|
||||
substrateCryptoType == other.substrateCryptoType &&
|
||||
ethereumAddress.contentEquals(other.ethereumAddress) &&
|
||||
ethereumPublicKey.contentEquals(other.ethereumPublicKey) &&
|
||||
name == other.name &&
|
||||
type == other.type &&
|
||||
chainAccounts == other.chainAccounts
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = walletId.hashCode()
|
||||
result = 31 * result + (substratePublicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (substrateAccountId?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (substrateCryptoType?.hashCode() ?: 0)
|
||||
result = 31 * result + (ethereumAddress?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (ethereumPublicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + type.hashCode()
|
||||
result = 31 * result + chainAccounts.hashCode()
|
||||
result = 31 * result + identifier.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class PrivateData(
|
||||
val wallets: List<WalletPrivateInfo>
|
||||
)
|
||||
|
||||
data class WalletPrivateInfo(
|
||||
val walletId: String,
|
||||
val entropy: ByteArray?,
|
||||
val substrate: SubstrateSecrets?,
|
||||
val ethereum: EthereumSecrets?,
|
||||
val chainAccounts: List<ChainAccountSecrets>,
|
||||
) : Identifiable {
|
||||
|
||||
override val identifier: String = walletId
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as WalletPrivateInfo
|
||||
|
||||
if (walletId != other.walletId) return false
|
||||
if (entropy != null) {
|
||||
if (other.entropy == null) return false
|
||||
if (!entropy.contentEquals(other.entropy)) return false
|
||||
} else if (other.entropy != null) return false
|
||||
if (substrate != other.substrate) return false
|
||||
if (ethereum != other.ethereum) return false
|
||||
if (chainAccounts != other.chainAccounts) return false
|
||||
return identifier == other.identifier
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = walletId.hashCode()
|
||||
result = 31 * result + (entropy?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (substrate?.hashCode() ?: 0)
|
||||
result = 31 * result + (ethereum?.hashCode() ?: 0)
|
||||
result = 31 * result + chainAccounts.hashCode()
|
||||
result = 31 * result + identifier.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
data class ChainAccountSecrets(
|
||||
val accountId: ByteArray,
|
||||
val entropy: ByteArray?,
|
||||
val seed: ByteArray?,
|
||||
val keypair: KeyPairSecrets?,
|
||||
val derivationPath: String?,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ChainAccountSecrets
|
||||
|
||||
if (!accountId.contentEquals(other.accountId)) return false
|
||||
if (entropy != null) {
|
||||
if (other.entropy == null) return false
|
||||
if (!entropy.contentEquals(other.entropy)) return false
|
||||
} else if (other.entropy != null) return false
|
||||
if (seed != null) {
|
||||
if (other.seed == null) return false
|
||||
if (!seed.contentEquals(other.seed)) return false
|
||||
} else if (other.seed != null) return false
|
||||
if (keypair != other.keypair) return false
|
||||
return derivationPath == other.derivationPath
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = accountId.contentHashCode()
|
||||
result = 31 * result + (entropy?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (seed?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (keypair?.hashCode() ?: 0)
|
||||
result = 31 * result + (derivationPath?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class SubstrateSecrets(
|
||||
val seed: ByteArray?,
|
||||
val keypair: KeyPairSecrets?,
|
||||
val derivationPath: String?,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SubstrateSecrets
|
||||
|
||||
if (seed != null) {
|
||||
if (other.seed == null) return false
|
||||
if (!seed.contentEquals(other.seed)) return false
|
||||
} else if (other.seed != null) return false
|
||||
if (keypair != other.keypair) return false
|
||||
return derivationPath == other.derivationPath
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = seed?.contentHashCode() ?: 0
|
||||
result = 31 * result + keypair.hashCode()
|
||||
result = 31 * result + (derivationPath?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
data class EthereumSecrets(
|
||||
val keypair: KeyPairSecrets,
|
||||
val derivationPath: String?,
|
||||
)
|
||||
|
||||
data class KeyPairSecrets(
|
||||
val publicKey: ByteArray,
|
||||
val privateKey: ByteArray,
|
||||
val nonce: ByteArray?
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as KeyPairSecrets
|
||||
|
||||
if (!publicKey.contentEquals(other.publicKey)) return false
|
||||
if (!privateKey.contentEquals(other.privateKey)) return false
|
||||
if (nonce != null) {
|
||||
if (other.nonce == null) return false
|
||||
if (!nonce.contentEquals(other.nonce)) return false
|
||||
} else if (other.nonce != null) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = publicKey.contentHashCode()
|
||||
result = 31 * result + privateKey.contentHashCode()
|
||||
result = 31 * result + (nonce?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun CloudBackup.WalletPrivateInfo.isCompletelyEmpty(): Boolean {
|
||||
return entropy == null && substrate == null && ethereum == null && chainAccounts.isEmpty()
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.InvalidBackupPasswordError
|
||||
|
||||
interface EncryptedCloudBackup {
|
||||
|
||||
val publicData: CloudBackup.PublicData
|
||||
|
||||
/**
|
||||
* @throws InvalidBackupPasswordError
|
||||
*/
|
||||
suspend fun decrypt(password: String): Result<CloudBackup>
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupAuthFailed
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupExistingBackupFound
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupNotEnoughSpace
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupServiceUnavailable
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupUnknownError
|
||||
|
||||
sealed class PreCreateValidationStatus {
|
||||
|
||||
object Ok : PreCreateValidationStatus()
|
||||
|
||||
object AuthenticationFailed : PreCreateValidationStatus(), CloudBackupAuthFailed
|
||||
|
||||
object BackupServiceUnavailable : PreCreateValidationStatus(), CloudBackupServiceUnavailable
|
||||
|
||||
object ExistingBackupFound : PreCreateValidationStatus(), CloudBackupExistingBackupFound
|
||||
|
||||
object NotEnoughSpace : PreCreateValidationStatus(), CloudBackupNotEnoughSpace
|
||||
|
||||
object OtherError : PreCreateValidationStatus(), CloudBackupUnknownError
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model
|
||||
|
||||
class WriteBackupRequest(
|
||||
val cloudBackup: CloudBackup,
|
||||
val password: String
|
||||
)
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff
|
||||
|
||||
import io.novafoundation.nova.common.utils.CollectionDiffer
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup.WalletPublicInfo
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.SourcedBackupChanges.WalletsFromCloud
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy.BackupDiffStrategyFactory
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy.addToCloud
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy.addToLocal
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy.modifyInCloud
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy.modifyLocally
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy.removeFromCloud
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy.removeLocally
|
||||
|
||||
class CloudBackupDiff(
|
||||
val localChanges: PerSourceDiff,
|
||||
val cloudChanges: PerSourceDiff
|
||||
) {
|
||||
|
||||
class PerSourceDiff(
|
||||
val added: List<WalletPublicInfo>,
|
||||
val modified: List<WalletPublicInfo>,
|
||||
val removed: List<WalletPublicInfo>
|
||||
)
|
||||
}
|
||||
|
||||
fun CloudBackupDiff.PerSourceDiff.isEmpty(): Boolean = added.isEmpty() && modified.isEmpty() && removed.isEmpty()
|
||||
|
||||
fun CloudBackupDiff.PerSourceDiff.isNotEmpty(): Boolean = !isEmpty()
|
||||
|
||||
fun CloudBackupDiff.PerSourceDiff.isDestructive(): Boolean = removed.isNotEmpty() || modified.isNotEmpty()
|
||||
|
||||
fun CloudBackupDiff.PerSourceDiff.isNotDestructive(): Boolean = !isDestructive()
|
||||
|
||||
/**
|
||||
* @see [CloudBackup.PublicData.localVsCloudDiff]
|
||||
*/
|
||||
fun CloudBackup.localVsCloudDiff(
|
||||
cloudVersion: CloudBackup,
|
||||
strategyFactory: BackupDiffStrategyFactory
|
||||
): CloudBackupDiff {
|
||||
return publicData.localVsCloudDiff(cloudVersion.publicData, strategyFactory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the diff between local and cloud versions
|
||||
*
|
||||
* [this] - Local snapshot
|
||||
* [cloudVersion] - Cloud snapshot
|
||||
*/
|
||||
fun CloudBackup.PublicData.localVsCloudDiff(
|
||||
cloudVersion: CloudBackup.PublicData,
|
||||
strategyFactory: BackupDiffStrategyFactory
|
||||
): CloudBackupDiff {
|
||||
val localVersion = this
|
||||
val strategy = strategyFactory(localVersion, cloudVersion)
|
||||
|
||||
val localToCloudDiff = CollectionDiffer.findDiff(newItems = cloudVersion.wallets, oldItems = localVersion.wallets, forceUseNewItems = false)
|
||||
|
||||
val walletsOnlyPresentInCloud = localToCloudDiff.added.asCloudWallets()
|
||||
val walletsModifiedByCloud = localToCloudDiff.updated.asCloudWallets()
|
||||
val walletsOnlyPresentLocally = localToCloudDiff.removed.asLocalWallets()
|
||||
|
||||
val cloudToLocalDiff = CollectionDiffer.findDiff(newItems = localVersion.wallets, oldItems = cloudVersion.wallets, forceUseNewItems = false)
|
||||
val walletsModifiedByLocal = cloudToLocalDiff.updated.asLocalWallets()
|
||||
|
||||
return CloudBackupDiff(
|
||||
localChanges = CloudBackupDiff.PerSourceDiff(
|
||||
added = strategy.addToLocal(walletsOnlyPresentInCloud),
|
||||
removed = strategy.removeLocally(walletsOnlyPresentLocally),
|
||||
modified = strategy.modifyLocally(walletsModifiedByCloud)
|
||||
),
|
||||
cloudChanges = CloudBackupDiff.PerSourceDiff(
|
||||
added = strategy.addToCloud(walletsOnlyPresentLocally),
|
||||
removed = strategy.removeFromCloud(walletsOnlyPresentInCloud),
|
||||
modified = strategy.modifyInCloud(walletsModifiedByLocal)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<WalletPublicInfo>.asCloudWallets(): SourcedBackupChanges<WalletsFromCloud> {
|
||||
return SourcedBackupChanges(this)
|
||||
}
|
||||
|
||||
private fun List<WalletPublicInfo>.asLocalWallets(): SourcedBackupChanges<SourcedBackupChanges.LocalWallets> {
|
||||
return SourcedBackupChanges(this)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
|
||||
|
||||
class SourcedBackupChanges<SOURCE>(val changes: List<CloudBackup.WalletPublicInfo>) {
|
||||
|
||||
object LocalWallets
|
||||
|
||||
object WalletsFromCloud
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.SourcedBackupChanges
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.SourcedBackupChanges.LocalWallets
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.SourcedBackupChanges.WalletsFromCloud
|
||||
|
||||
typealias BackupDiffStrategyFactory = (localData: CloudBackup.PublicData, remoteData: CloudBackup.PublicData) -> BackupDiffStrategy
|
||||
|
||||
interface BackupDiffStrategy {
|
||||
|
||||
companion object {
|
||||
|
||||
fun importFromCloud(): BackupDiffStrategyFactory = { _, _ ->
|
||||
ImportFromCloudStrategy()
|
||||
}
|
||||
|
||||
fun syncWithCloud(): BackupDiffStrategyFactory = { local, remote ->
|
||||
SyncWithCloudStrategy(localTimestamp = local.modifiedAt, cloudTimestamp = remote.modifiedAt)
|
||||
}
|
||||
|
||||
fun overwriteLocal(): BackupDiffStrategyFactory = { _, _ ->
|
||||
OverwriteLocalStrategy()
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldAddLocally(walletsOnlyPresentInCloud: SourcedBackupChanges<WalletsFromCloud>): Boolean
|
||||
|
||||
fun shouldRemoveFromCloud(walletsOnlyPresentInCloud: SourcedBackupChanges<WalletsFromCloud>): Boolean
|
||||
|
||||
fun shouldRemoveLocally(walletsOnlyPresentLocally: SourcedBackupChanges<LocalWallets>): Boolean
|
||||
|
||||
fun shouldAddToCloud(walletsOnlyPresentLocally: SourcedBackupChanges<LocalWallets>): Boolean
|
||||
|
||||
fun shouldModifyLocally(modifiedInCloud: SourcedBackupChanges<WalletsFromCloud>): Boolean
|
||||
|
||||
fun shouldModifyInCloud(modifiedLocally: SourcedBackupChanges<LocalWallets>): Boolean
|
||||
}
|
||||
|
||||
fun BackupDiffStrategy.addToLocal(walletsOnlyPresentInCloud: SourcedBackupChanges<WalletsFromCloud>): List<CloudBackup.WalletPublicInfo> {
|
||||
return walletsOnlyPresentInCloud.takeValueOrEmpty(shouldAddLocally(walletsOnlyPresentInCloud))
|
||||
}
|
||||
|
||||
fun BackupDiffStrategy.removeFromCloud(walletsOnlyPresentInCloud: SourcedBackupChanges<WalletsFromCloud>): List<CloudBackup.WalletPublicInfo> {
|
||||
return walletsOnlyPresentInCloud.takeValueOrEmpty(shouldRemoveFromCloud(walletsOnlyPresentInCloud))
|
||||
}
|
||||
|
||||
fun BackupDiffStrategy.removeLocally(walletsOnlyPresentLocally: SourcedBackupChanges<LocalWallets>): List<CloudBackup.WalletPublicInfo> {
|
||||
return walletsOnlyPresentLocally.takeValueOrEmpty(shouldRemoveLocally(walletsOnlyPresentLocally))
|
||||
}
|
||||
|
||||
fun BackupDiffStrategy.addToCloud(walletsOnlyPresentLocally: SourcedBackupChanges<LocalWallets>): List<CloudBackup.WalletPublicInfo> {
|
||||
return walletsOnlyPresentLocally.takeValueOrEmpty(shouldAddToCloud(walletsOnlyPresentLocally))
|
||||
}
|
||||
|
||||
fun BackupDiffStrategy.modifyLocally(modifiedInCloud: SourcedBackupChanges<WalletsFromCloud>): List<CloudBackup.WalletPublicInfo> {
|
||||
return modifiedInCloud.takeValueOrEmpty(shouldModifyLocally(modifiedInCloud))
|
||||
}
|
||||
|
||||
fun BackupDiffStrategy.modifyInCloud(modifiedLocally: SourcedBackupChanges<LocalWallets>): List<CloudBackup.WalletPublicInfo> {
|
||||
return modifiedLocally.takeValueOrEmpty(shouldModifyInCloud(modifiedLocally))
|
||||
}
|
||||
|
||||
private fun SourcedBackupChanges<*>.takeValueOrEmpty(condition: Boolean): List<CloudBackup.WalletPublicInfo> {
|
||||
return if (condition) {
|
||||
changes
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.SourcedBackupChanges
|
||||
|
||||
/**
|
||||
* Strategy that retains both local-exclusive and cloud-exclusive wallets
|
||||
* Modified wallets are taken from local state and updated in cloud
|
||||
*/
|
||||
class ImportFromCloudStrategy : BackupDiffStrategy {
|
||||
|
||||
override fun shouldAddLocally(walletsOnlyPresentInCloud: SourcedBackupChanges<SourcedBackupChanges.WalletsFromCloud>): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun shouldRemoveFromCloud(walletsOnlyPresentInCloud: SourcedBackupChanges<SourcedBackupChanges.WalletsFromCloud>): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun shouldRemoveLocally(walletsOnlyPresentLocally: SourcedBackupChanges<SourcedBackupChanges.LocalWallets>): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun shouldAddToCloud(walletsOnlyPresentLocally: SourcedBackupChanges<SourcedBackupChanges.LocalWallets>): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun shouldModifyLocally(modifiedInCloud: SourcedBackupChanges<SourcedBackupChanges.WalletsFromCloud>): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun shouldModifyInCloud(modifiedLocally: SourcedBackupChanges<SourcedBackupChanges.LocalWallets>): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.SourcedBackupChanges
|
||||
|
||||
/**
|
||||
* Strategy that considers cloud version to be the source of truth
|
||||
*/
|
||||
class OverwriteLocalStrategy : BackupDiffStrategy {
|
||||
|
||||
override fun shouldAddLocally(walletsOnlyPresentInCloud: SourcedBackupChanges<SourcedBackupChanges.WalletsFromCloud>): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun shouldRemoveFromCloud(walletsOnlyPresentInCloud: SourcedBackupChanges<SourcedBackupChanges.WalletsFromCloud>): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun shouldRemoveLocally(walletsOnlyPresentLocally: SourcedBackupChanges<SourcedBackupChanges.LocalWallets>): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun shouldAddToCloud(walletsOnlyPresentLocally: SourcedBackupChanges<SourcedBackupChanges.LocalWallets>): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun shouldModifyLocally(modifiedInCloud: SourcedBackupChanges<SourcedBackupChanges.WalletsFromCloud>): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun shouldModifyInCloud(modifiedLocally: SourcedBackupChanges<SourcedBackupChanges.LocalWallets>): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.SourcedBackupChanges
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.SourcedBackupChanges.LocalWallets
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.SourcedBackupChanges.WalletsFromCloud
|
||||
|
||||
/**
|
||||
* Strategy that uses modifiedAt timestamps to identify priority
|
||||
*/
|
||||
class SyncWithCloudStrategy(
|
||||
localTimestamp: Long,
|
||||
cloudTimestamp: Long
|
||||
) : BackupDiffStrategy {
|
||||
|
||||
private val cloudInPriority = cloudTimestamp > localTimestamp
|
||||
private val localInPriority = localTimestamp > cloudTimestamp
|
||||
|
||||
override fun shouldAddLocally(walletsOnlyPresentInCloud: SourcedBackupChanges<WalletsFromCloud>): Boolean {
|
||||
return cloudInPriority
|
||||
}
|
||||
|
||||
override fun shouldRemoveFromCloud(walletsOnlyPresentInCloud: SourcedBackupChanges<WalletsFromCloud>): Boolean {
|
||||
return localInPriority
|
||||
}
|
||||
|
||||
override fun shouldRemoveLocally(walletsOnlyPresentLocally: SourcedBackupChanges<LocalWallets>): Boolean {
|
||||
return cloudInPriority
|
||||
}
|
||||
|
||||
override fun shouldAddToCloud(walletsOnlyPresentLocally: SourcedBackupChanges<LocalWallets>): Boolean {
|
||||
return localInPriority
|
||||
}
|
||||
|
||||
override fun shouldModifyLocally(modifiedInCloud: SourcedBackupChanges<WalletsFromCloud>): Boolean {
|
||||
return cloudInPriority
|
||||
}
|
||||
|
||||
override fun shouldModifyInCloud(modifiedLocally: SourcedBackupChanges<LocalWallets>): Boolean {
|
||||
return localInPriority
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.CloudBackupDiff
|
||||
|
||||
interface CloudBackupAuthFailed
|
||||
|
||||
interface CloudBackupServiceUnavailable
|
||||
|
||||
interface CloudBackupExistingBackupFound
|
||||
|
||||
interface CloudBackupNotFound
|
||||
|
||||
interface CloudBackupNotEnoughSpace
|
||||
|
||||
interface CloudBackupUnknownError
|
||||
|
||||
interface CorruptedBackupError
|
||||
|
||||
class CannotApplyNonDestructiveDiff(val cloudBackupDiff: CloudBackupDiff, val cloudBackup: CloudBackup) : Throwable()
|
||||
|
||||
class PasswordNotSaved : Throwable()
|
||||
|
||||
class InvalidBackupPasswordError : Throwable()
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors
|
||||
|
||||
sealed class DeleteBackupError : Throwable() {
|
||||
|
||||
object Other : DeleteBackupError(), CloudBackupUnknownError
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors
|
||||
|
||||
sealed class FetchBackupError : Throwable() {
|
||||
|
||||
object AuthFailed : FetchBackupError(), CloudBackupAuthFailed
|
||||
|
||||
object BackupNotFound : FetchBackupError(), CloudBackupNotFound
|
||||
|
||||
object CorruptedBackup : FetchBackupError(), CorruptedBackupError
|
||||
|
||||
object Other : FetchBackupError(), CloudBackupUnknownError
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors
|
||||
|
||||
sealed class WriteBackupError : Throwable() {
|
||||
|
||||
object Other : WriteBackupError(), CloudBackupUnknownError
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.action
|
||||
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.addColor
|
||||
import io.novafoundation.nova.common.utils.formatting.spannable.spannableFormatting
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncher
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ButtonPreferences
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.negative
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.primary
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.secondary
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.R
|
||||
|
||||
fun ActionBottomSheetLauncher.launchBackupLostPasswordAction(resourceManager: ResourceManager, onDeleteClicked: () -> Unit) {
|
||||
launchBottomSheet(
|
||||
imageRes = R.drawable.ic_cloud_backup_password,
|
||||
title = resourceManager.getString(R.string.restore_cloud_backup_delete_backup_title),
|
||||
subtitle = with(resourceManager) {
|
||||
val highlightedFirstPart = getString(R.string.restore_cloud_backup_delete_backup_description_highlighted_1)
|
||||
.addColor(getColor(R.color.text_primary))
|
||||
|
||||
val highlightedSecondPart = getString(R.string.restore_cloud_backup_delete_backup_description_highlighted_2)
|
||||
.addColor(getColor(R.color.text_primary))
|
||||
|
||||
getString(R.string.restore_cloud_backup_delete_backup_description).spannableFormatting(highlightedFirstPart, highlightedSecondPart)
|
||||
},
|
||||
neutralButtonPreferences = ButtonPreferences.secondary(
|
||||
resourceManager.getString(
|
||||
R.string.common_cancel
|
||||
)
|
||||
),
|
||||
actionButtonPreferences = ButtonPreferences.negative(
|
||||
resourceManager.getString(R.string.cloud_backup_delete_button),
|
||||
onDeleteClicked
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun ActionBottomSheetLauncher.launchDeleteBackupAction(resourceManager: ResourceManager, onDeleteClicked: () -> Unit) {
|
||||
launchBottomSheet(
|
||||
imageRes = R.drawable.ic_cloud_backup_delete,
|
||||
title = resourceManager.getString(R.string.cloud_backup_delete_action_title),
|
||||
subtitle = with(resourceManager) {
|
||||
val highlightedPart = getString(R.string.cloud_backup_delete_action_subtitle_highlighted)
|
||||
.addColor(getColor(R.color.text_primary))
|
||||
getString(R.string.cloud_backup_delete_action_subtitle).spannableFormatting(highlightedPart)
|
||||
},
|
||||
neutralButtonPreferences = ButtonPreferences.secondary(
|
||||
resourceManager.getString(
|
||||
R.string.common_cancel
|
||||
)
|
||||
),
|
||||
actionButtonPreferences = ButtonPreferences.negative(
|
||||
resourceManager.getString(R.string.cloud_backup_delete_button),
|
||||
onDeleteClicked
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun ActionBottomSheetLauncher.launchRememberPasswordWarning(resourceManager: ResourceManager) {
|
||||
launchBottomSheet(
|
||||
imageRes = R.drawable.ic_cloud_backup_lock,
|
||||
title = resourceManager.getString(R.string.create_cloud_backup_password_alert_title),
|
||||
subtitle = with(resourceManager) {
|
||||
val highlightedPart = getString(R.string.create_cloud_backup_password_alert_subtitle_highlighted)
|
||||
.addColor(getColor(R.color.text_primary))
|
||||
|
||||
getString(R.string.create_cloud_backup_password_alert_subtitle).spannableFormatting(highlightedPart)
|
||||
},
|
||||
actionButtonPreferences = ButtonPreferences.primary(resourceManager.getString(R.string.common_got_it))
|
||||
)
|
||||
}
|
||||
|
||||
fun ActionBottomSheetLauncher.launchCorruptedBackupFoundAction(resourceManager: ResourceManager, onDeleteClicked: () -> Unit) {
|
||||
launchBottomSheet(
|
||||
imageRes = R.drawable.ic_cloud_backup_error,
|
||||
title = resourceManager.getString(R.string.corrupted_backup_error_title),
|
||||
subtitle = with(resourceManager) {
|
||||
val highlightedPart = getString(R.string.corrupted_backup_error_subtitle_highlighted)
|
||||
.addColor(getColor(R.color.text_primary))
|
||||
|
||||
getString(R.string.corrupted_backup_error_subtitle).spannableFormatting(highlightedPart)
|
||||
},
|
||||
neutralButtonPreferences = ButtonPreferences.secondary(resourceManager.getString(R.string.common_cancel)),
|
||||
actionButtonPreferences = ButtonPreferences.negative(
|
||||
resourceManager.getString(R.string.cloud_backup_delete_button),
|
||||
onDeleteClicked
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun ActionBottomSheetLauncher.launchExistingCloudBackupAction(resourceManager: ResourceManager, onImportClicked: () -> Unit) {
|
||||
launchBottomSheet(
|
||||
imageRes = R.drawable.ic_cloud_backup_sync,
|
||||
title = resourceManager.getString(R.string.existing_cloud_backup_found_title),
|
||||
subtitle = with(resourceManager) {
|
||||
val highlightedPart = getString(R.string.existing_cloud_backup_found_subtitle_highlight)
|
||||
.addColor(getColor(R.color.text_primary))
|
||||
|
||||
getString(R.string.existing_cloud_backup_found_subtitle).spannableFormatting(highlightedPart)
|
||||
},
|
||||
neutralButtonPreferences = ButtonPreferences.secondary(resourceManager.getString(R.string.common_cancel)),
|
||||
actionButtonPreferences = ButtonPreferences.primary(
|
||||
resourceManager.getString(R.string.existing_cloud_backup_found_button),
|
||||
onImportClicked
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun ActionBottomSheetLauncher.launchDeprecatedPasswordAction(resourceManager: ResourceManager, onEnterPasswordClick: () -> Unit) {
|
||||
launchBottomSheet(
|
||||
imageRes = R.drawable.ic_cloud_backup_password,
|
||||
title = resourceManager.getString(R.string.deprecated_cloud_backup_password_title),
|
||||
subtitle = with(resourceManager) {
|
||||
val highlightedPart = getString(R.string.deprecated_cloud_backup_password_subtitle_highlight)
|
||||
.addColor(getColor(R.color.text_primary))
|
||||
|
||||
getString(R.string.deprecated_cloud_backup_password_subtitle).spannableFormatting(highlightedPart)
|
||||
},
|
||||
neutralButtonPreferences = ButtonPreferences.secondary(resourceManager.getString(R.string.common_not_now)),
|
||||
actionButtonPreferences = ButtonPreferences.primary(
|
||||
resourceManager.getString(R.string.common_enter_password),
|
||||
onEnterPasswordClick
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun ActionBottomSheetLauncher.launchCloudBackupChangesAction(resourceManager: ResourceManager, onReviewClicked: () -> Unit) {
|
||||
launchBottomSheet(
|
||||
imageRes = R.drawable.ic_cloud_backup_warning,
|
||||
title = resourceManager.getString(R.string.cloud_backup_destructive_changes_action_title),
|
||||
subtitle = resourceManager.getString(R.string.cloud_backup_destructive_changes_action_subtitle),
|
||||
neutralButtonPreferences = ButtonPreferences.secondary(
|
||||
resourceManager.getString(R.string.common_not_now)
|
||||
),
|
||||
actionButtonPreferences = ButtonPreferences.primary(
|
||||
resourceManager.getString(R.string.cloud_backup_destructive_changes_button),
|
||||
onReviewClicked
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun ActionBottomSheetLauncher.launchCloudBackupDestructiveChangesNotApplied(
|
||||
resourceManager: ResourceManager,
|
||||
onReviewClicked: () -> Unit
|
||||
) {
|
||||
launchBottomSheet(
|
||||
imageRes = R.drawable.ic_cloud_backup_warning,
|
||||
title = resourceManager.getString(R.string.cloud_backup_destructive_changes_not_applied_title),
|
||||
subtitle = resourceManager.getString(R.string.cloud_backup_destructive_changes_not_applied_subtitle),
|
||||
neutralButtonPreferences = ButtonPreferences.secondary(
|
||||
resourceManager.getString(R.string.common_not_now)
|
||||
),
|
||||
actionButtonPreferences = ButtonPreferences.negative(
|
||||
resourceManager.getString(R.string.cloud_backup_destructive_changes_not_applied_button),
|
||||
onReviewClicked
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun ActionBottomSheetLauncher.launchCloudBackupDestructiveChangesNotAppliedWithoutRouting(
|
||||
resourceManager: ResourceManager
|
||||
) {
|
||||
launchBottomSheet(
|
||||
imageRes = R.drawable.ic_cloud_backup_warning,
|
||||
title = resourceManager.getString(R.string.cloud_backup_destructive_changes_not_applied_title),
|
||||
subtitle = resourceManager.getString(R.string.cloud_backup_destructive_changes_not_applied_subtitle),
|
||||
actionButtonPreferences = ButtonPreferences.secondary(
|
||||
resourceManager.getString(R.string.common_got_it)
|
||||
)
|
||||
)
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.confirmation
|
||||
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ConfirmationAwaitable
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ConfirmationDialogInfo
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.fromRes
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.R
|
||||
|
||||
suspend fun ConfirmationAwaitable<ConfirmationDialogInfo>.awaitDeleteBackupConfirmation(resourceManager: ResourceManager) {
|
||||
awaitAction(
|
||||
ConfirmationDialogInfo.fromRes(
|
||||
resourceManager,
|
||||
title = R.string.cloud_backup_delete_backup_confirmation_title,
|
||||
message = R.string.cloud_backup_delete_backup_confirmation_message,
|
||||
positiveButton = R.string.cloud_backup_delete_button,
|
||||
negativeButton = R.string.common_cancel
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun ConfirmationAwaitable<ConfirmationDialogInfo>.awaitBackupDestructiveChangesConfirmation(resourceManager: ResourceManager) {
|
||||
awaitAction(
|
||||
ConfirmationDialogInfo.fromRes(
|
||||
resourceManager,
|
||||
title = R.string.cloud_backup_destructive_changes_confirmation_title,
|
||||
message = R.string.cloud_backup_destructive_changes_confirmation_subtitle,
|
||||
positiveButton = R.string.common_apply,
|
||||
negativeButton = R.string.common_cancel
|
||||
)
|
||||
)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling
|
||||
|
||||
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer
|
||||
import io.novafoundation.nova.common.mixin.api.toCustomDialogPayload
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupAuthFailed
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupNotEnoughSpace
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.InvalidBackupPasswordError
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupAuthFailed
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupInvalidPassword
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupNotEnoughSpace
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupUnknownError
|
||||
|
||||
fun mapChangePasswordValidationStatusToUi(
|
||||
resourceManager: ResourceManager,
|
||||
status: Throwable,
|
||||
initSignIn: () -> Unit
|
||||
): CustomDialogDisplayer.Payload {
|
||||
return when (status) {
|
||||
is CloudBackupAuthFailed -> handleCloudBackupAuthFailed(resourceManager, initSignIn)
|
||||
|
||||
is CloudBackupNotEnoughSpace -> handleCloudBackupNotEnoughSpace(resourceManager).toCustomDialogPayload(resourceManager)
|
||||
|
||||
is InvalidBackupPasswordError -> handleCloudBackupInvalidPassword(resourceManager).toCustomDialogPayload(resourceManager)
|
||||
|
||||
else -> handleCloudBackupUnknownError(resourceManager).toCustomDialogPayload(resourceManager)
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling
|
||||
|
||||
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer
|
||||
import io.novafoundation.nova.common.mixin.api.toCustomDialogPayload
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupAuthFailed
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupServiceUnavailable
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupAuthFailed
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupServiceUnavailable
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupUnknownError
|
||||
|
||||
fun mapCheckBackupAvailableFailureToUi(
|
||||
resourceManager: ResourceManager,
|
||||
throwable: Throwable,
|
||||
initSignIn: () -> Unit
|
||||
): CustomDialogDisplayer.Payload {
|
||||
return when (throwable) {
|
||||
is CloudBackupAuthFailed -> handleCloudBackupAuthFailed(resourceManager, initSignIn)
|
||||
|
||||
is CloudBackupServiceUnavailable -> handleCloudBackupServiceUnavailable(resourceManager).toCustomDialogPayload(resourceManager)
|
||||
|
||||
else -> handleCloudBackupUnknownError(resourceManager).toCustomDialogPayload(resourceManager)
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling
|
||||
|
||||
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer
|
||||
import io.novafoundation.nova.common.mixin.api.toCustomDialogPayload
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.PreCreateValidationStatus
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupAuthFailed
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupNotEnoughSpace
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupServiceUnavailable
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupUnknownError
|
||||
|
||||
fun mapPreCreateValidationStatusToUi(
|
||||
resourceManager: ResourceManager,
|
||||
status: PreCreateValidationStatus,
|
||||
existingBackupFound: () -> Unit,
|
||||
initSignIn: () -> Unit
|
||||
): CustomDialogDisplayer.Payload? {
|
||||
return when (status) {
|
||||
is PreCreateValidationStatus.AuthenticationFailed -> handleCloudBackupAuthFailed(resourceManager, initSignIn)
|
||||
|
||||
is PreCreateValidationStatus.BackupServiceUnavailable -> handleCloudBackupServiceUnavailable(resourceManager).toCustomDialogPayload(resourceManager)
|
||||
|
||||
is PreCreateValidationStatus.ExistingBackupFound -> {
|
||||
existingBackupFound()
|
||||
null
|
||||
}
|
||||
|
||||
is PreCreateValidationStatus.NotEnoughSpace -> handleCloudBackupNotEnoughSpace(resourceManager).toCustomDialogPayload(resourceManager)
|
||||
|
||||
is PreCreateValidationStatus.OtherError -> handleCloudBackupUnknownError(resourceManager).toCustomDialogPayload(resourceManager)
|
||||
|
||||
is PreCreateValidationStatus.Ok -> null
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling
|
||||
|
||||
import io.novafoundation.nova.common.base.TitleAndMessage
|
||||
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupNotFound
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupUnknownError
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CorruptedBackupError
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.InvalidBackupPasswordError
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupInvalidPassword
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupNotFound
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupUnknownError
|
||||
|
||||
fun mapRestoreBackupFailureToUi(
|
||||
resourceManager: ResourceManager,
|
||||
throwable: Throwable,
|
||||
corruptedBackupFound: () -> Unit
|
||||
): TitleAndMessage? {
|
||||
return when (throwable) {
|
||||
is InvalidBackupPasswordError -> handleCloudBackupInvalidPassword(resourceManager)
|
||||
|
||||
is CorruptedBackupError -> {
|
||||
corruptedBackupFound()
|
||||
return null
|
||||
}
|
||||
|
||||
is CloudBackupNotFound -> handleCloudBackupNotFound(resourceManager)
|
||||
|
||||
is CloudBackupUnknownError -> handleCloudBackupUnknownError(resourceManager)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun mapDeleteBackupFailureToUi(
|
||||
resourceManager: ResourceManager,
|
||||
throwable: Throwable
|
||||
): TitleAndMessage? {
|
||||
return when (throwable) {
|
||||
is CloudBackupUnknownError -> handleCloudBackupUnknownError(resourceManager)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun mapCheckPasswordFailureToUi(
|
||||
resourceManager: ResourceManager,
|
||||
throwable: Throwable
|
||||
): TitleAndMessage? {
|
||||
return when (throwable) {
|
||||
is InvalidBackupPasswordError -> handleCloudBackupInvalidPassword(resourceManager)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun mapRestorePasswordFailureToUi(
|
||||
resourceManager: ResourceManager,
|
||||
throwable: Throwable,
|
||||
corruptedBackupFound: () -> Unit
|
||||
): TitleAndMessage? {
|
||||
return when (throwable) {
|
||||
is InvalidBackupPasswordError -> handleCloudBackupInvalidPassword(resourceManager)
|
||||
|
||||
is CorruptedBackupError -> {
|
||||
corruptedBackupFound()
|
||||
null
|
||||
}
|
||||
|
||||
is CloudBackupNotFound -> handleCloudBackupNotFound(resourceManager)
|
||||
|
||||
else -> handleCloudBackupUnknownError(resourceManager)
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling
|
||||
|
||||
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer
|
||||
import io.novafoundation.nova.common.mixin.api.toCustomDialogPayload
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.CloudBackupDiff
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CannotApplyNonDestructiveDiff
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupAuthFailed
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupUnknownError
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CorruptedBackupError
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.InvalidBackupPasswordError
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.PasswordNotSaved
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupAuthFailed
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupUnknownError
|
||||
|
||||
fun mapCloudBackupSyncFailed(
|
||||
resourceManager: ResourceManager,
|
||||
state: Throwable,
|
||||
onPasswordDeprecated: () -> Unit,
|
||||
onCorruptedBackup: () -> Unit,
|
||||
initSignIn: () -> Unit,
|
||||
onDestructiveBackupFound: (CloudBackupDiff, CloudBackup) -> Unit,
|
||||
): CustomDialogDisplayer.Payload? {
|
||||
return when (state) {
|
||||
is CloudBackupAuthFailed -> handleCloudBackupAuthFailed(resourceManager, initSignIn)
|
||||
|
||||
is CloudBackupUnknownError -> handleCloudBackupUnknownError(resourceManager)
|
||||
.toCustomDialogPayload(resourceManager)
|
||||
|
||||
is CannotApplyNonDestructiveDiff -> {
|
||||
onDestructiveBackupFound(state.cloudBackupDiff, state.cloudBackup)
|
||||
null
|
||||
}
|
||||
|
||||
is CorruptedBackupError -> {
|
||||
onCorruptedBackup()
|
||||
null
|
||||
}
|
||||
|
||||
is InvalidBackupPasswordError, is PasswordNotSaved -> {
|
||||
onPasswordDeprecated()
|
||||
null
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling
|
||||
|
||||
import io.novafoundation.nova.common.base.TitleAndMessage
|
||||
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.errors.CloudBackupNotEnoughSpace
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupNotEnoughSpace
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.handleCloudBackupUnknownError
|
||||
|
||||
fun mapWriteBackupFailureToUi(
|
||||
resourceManager: ResourceManager,
|
||||
throwable: Throwable
|
||||
): TitleAndMessage {
|
||||
return when (throwable) {
|
||||
is CloudBackupNotEnoughSpace -> handleCloudBackupNotEnoughSpace(resourceManager)
|
||||
|
||||
else -> handleCloudBackupUnknownError(resourceManager)
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers
|
||||
|
||||
import io.novafoundation.nova.common.base.TitleAndMessage
|
||||
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer.Payload
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.R
|
||||
|
||||
fun handleCloudBackupAuthFailed(resourceManager: ResourceManager, onSignInClicked: () -> Unit): Payload {
|
||||
return Payload(
|
||||
resourceManager.getString(R.string.cloud_backup_error_google_service_auth_failed_title),
|
||||
resourceManager.getString(R.string.cloud_backup_error_google_service_auth_failed_message),
|
||||
Payload.DialogAction(resourceManager.getString(R.string.common_sign_in), onSignInClicked),
|
||||
Payload.DialogAction.noOp(resourceManager.getString(R.string.common_cancel))
|
||||
)
|
||||
}
|
||||
|
||||
fun handleCloudBackupServiceUnavailable(resourceManager: ResourceManager): TitleAndMessage {
|
||||
return TitleAndMessage(
|
||||
resourceManager.getString(R.string.cloud_backup_error_google_service_not_found_title),
|
||||
resourceManager.getString(R.string.cloud_backup_error_google_service_not_found_message)
|
||||
)
|
||||
}
|
||||
|
||||
fun handleCloudBackupNotEnoughSpace(resourceManager: ResourceManager): TitleAndMessage {
|
||||
return TitleAndMessage(
|
||||
resourceManager.getString(R.string.cloud_backup_error_google_service_not_enough_space_title),
|
||||
resourceManager.getString(R.string.cloud_backup_error_google_service_not_enough_space_message)
|
||||
)
|
||||
}
|
||||
|
||||
fun handleCloudBackupNotFound(resourceManager: ResourceManager): TitleAndMessage {
|
||||
return TitleAndMessage(
|
||||
resourceManager.getString(R.string.cloud_backup_error_not_found_title),
|
||||
resourceManager.getString(R.string.cloud_backup_error_not_found_message)
|
||||
)
|
||||
}
|
||||
|
||||
fun handleCloudBackupUnknownError(resourceManager: ResourceManager): TitleAndMessage {
|
||||
return TitleAndMessage(
|
||||
resourceManager.getString(R.string.cloud_backup_error_google_service_other_title),
|
||||
resourceManager.getString(R.string.cloud_backup_error_google_service_other_message)
|
||||
)
|
||||
}
|
||||
|
||||
fun handleCloudBackupInvalidPassword(resourceManager: ResourceManager): TitleAndMessage {
|
||||
return TitleAndMessage(
|
||||
resourceManager.getString(R.string.cloud_backup_error_invalid_password_title),
|
||||
resourceManager.getString(R.string.cloud_backup_error_invalid_password_message),
|
||||
)
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.base.showError
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
|
||||
fun BaseViewModel.showCloudBackupUnknownError(resourceManager: ResourceManager) {
|
||||
showError(handleCloudBackupUnknownError(resourceManager))
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.mixin
|
||||
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface CloudBackupChangingWarningMixinFactory {
|
||||
|
||||
fun create(coroutineScope: CoroutineScope): CloudBackupChangingWarningMixin
|
||||
}
|
||||
|
||||
interface CloudBackupChangingWarningMixin {
|
||||
|
||||
val actionBottomSheetLauncher: ActionBottomSheetLauncher
|
||||
|
||||
fun launchChangingConfirmationIfNeeded(onConfirm: () -> Unit)
|
||||
|
||||
fun launchRemovingConfirmationIfNeeded(onConfirm: () -> Unit)
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_cloud_backup_api.presenter.mixin
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseScreenMixin
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.observeActionBottomSheet
|
||||
|
||||
fun BaseScreenMixin<*>.observeConfirmationAction(mixinFactory: CloudBackupChangingWarningMixin) {
|
||||
observeActionBottomSheet(mixinFactory.actionBottomSheetLauncher)
|
||||
}
|
||||
Reference in New Issue
Block a user