mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 11:28:01 +00:00
Initial commit: Pezkuwi Wallet Android
Security hardened release: - Code obfuscation enabled (minifyEnabled=true, shrinkResources=true) - Sensitive files excluded (google-services.json, keystores) - Branch.io key moved to BuildConfig placeholder - Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77 - Comprehensive ProGuard rules for crypto wallet - Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
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>
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package io.novafoundation.nova.feature_settings_impl
|
||||
|
||||
import io.novafoundation.nova.common.navigation.ReturnableRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.AddNetworkPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.ChainNetworkManagementPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node.CustomNodePayload
|
||||
|
||||
interface SettingsRouter : ReturnableRouter {
|
||||
|
||||
fun openWallets()
|
||||
|
||||
fun openNetworks()
|
||||
|
||||
fun openNetworkDetails(payload: ChainNetworkManagementPayload)
|
||||
|
||||
fun openCustomNode(payload: CustomNodePayload)
|
||||
|
||||
fun openPushNotificationSettings()
|
||||
|
||||
fun openCurrencies()
|
||||
|
||||
fun openLanguages()
|
||||
|
||||
fun openAppearance()
|
||||
|
||||
fun openChangePinCode()
|
||||
|
||||
fun openWalletDetails(metaId: Long)
|
||||
|
||||
fun openSwitchWallet()
|
||||
|
||||
fun openWalletConnectScan()
|
||||
|
||||
fun openWalletConnectSessions()
|
||||
|
||||
fun openCloudBackupSettings()
|
||||
|
||||
fun openManualBackup()
|
||||
|
||||
fun addNetwork()
|
||||
|
||||
fun openCreateNetworkFlow()
|
||||
|
||||
fun openCreateNetworkFlow(payload: AddNetworkPayload.Mode.Add)
|
||||
|
||||
fun finishCreateNetworkFlow()
|
||||
|
||||
fun openEditNetwork(payload: AddNetworkPayload.Mode.Edit)
|
||||
|
||||
fun returnToWallet()
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.data
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.calls.GetBlockHashRequest
|
||||
import io.novafoundation.nova.common.utils.removeHexPrefix
|
||||
import io.novafoundation.nova.core.ethereum.Web3Api
|
||||
import io.novafoundation.nova.runtime.ethereum.Web3ApiFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.sendSuspend
|
||||
import io.novafoundation.nova.runtime.ext.evmChainIdFrom
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.NetworkType
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnection
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory
|
||||
import io.novasama.substrate_sdk_android.wsrpc.executeAsync
|
||||
import io.novasama.substrate_sdk_android.wsrpc.mappers.nonNull
|
||||
import io.novasama.substrate_sdk_android.wsrpc.mappers.pojo
|
||||
import java.math.BigInteger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface NodeChainIdRepository {
|
||||
|
||||
suspend fun requestChainId(): String
|
||||
}
|
||||
|
||||
class NodeChainIdRepositoryFactory(
|
||||
private val nodeConnectionFactory: NodeConnectionFactory,
|
||||
private val web3ApiFactory: Web3ApiFactory
|
||||
) {
|
||||
fun create(networkType: NetworkType, nodeUrl: String, coroutineScope: CoroutineScope): NodeChainIdRepository {
|
||||
val nodeConnection = nodeConnectionFactory.createNodeConnection(nodeUrl, coroutineScope)
|
||||
|
||||
return create(networkType, nodeConnection)
|
||||
}
|
||||
|
||||
fun create(networkType: NetworkType, nodeConnection: NodeConnection): NodeChainIdRepository {
|
||||
return when (networkType) {
|
||||
NetworkType.SUBSTRATE -> substrate(nodeConnection)
|
||||
|
||||
NetworkType.EVM -> evm(nodeConnection)
|
||||
}
|
||||
}
|
||||
|
||||
fun substrate(nodeConnection: NodeConnection): SubstrateNodeChainIdRepository {
|
||||
return SubstrateNodeChainIdRepository(nodeConnection)
|
||||
}
|
||||
|
||||
fun evm(nodeConnection: NodeConnection): EthereumNodeChainIdRepository {
|
||||
return EthereumNodeChainIdRepository(nodeConnection, web3ApiFactory)
|
||||
}
|
||||
}
|
||||
|
||||
class SubstrateNodeChainIdRepository(
|
||||
private val nodeConnection: NodeConnection
|
||||
) : NodeChainIdRepository {
|
||||
|
||||
override suspend fun requestChainId(): String {
|
||||
val genesisHash = nodeConnection.getSocketService().executeAsync(
|
||||
GetBlockHashRequest(BigInteger.ZERO),
|
||||
mapper = pojo<String>().nonNull()
|
||||
)
|
||||
|
||||
return genesisHash.removeHexPrefix()
|
||||
}
|
||||
}
|
||||
|
||||
class EthereumNodeChainIdRepository(
|
||||
private val nodeConnection: NodeConnection,
|
||||
private val web3ApiFactory: Web3ApiFactory
|
||||
) : NodeChainIdRepository {
|
||||
|
||||
private val web3Api = createWeb3Api()
|
||||
|
||||
override suspend fun requestChainId(): String {
|
||||
val chainId = web3Api.ethChainId().sendSuspend().chainId
|
||||
|
||||
return evmChainIdFrom(chainId)
|
||||
}
|
||||
|
||||
private fun createWeb3Api(): Web3Api {
|
||||
return web3ApiFactory.createWss(nodeConnection.getSocketService())
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.di
|
||||
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import io.novafoundation.nova.common.di.CommonApi
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.ChangeBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.RestoreBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.createPassword.SyncWalletsBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.di.CloudBackupFeatureApi
|
||||
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
|
||||
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.assetIcons.di.AppearanceComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.main.di.NetworkManagementListComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.addedNetworks.di.AddedNetworkListComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings.di.CloudBackupSettingsComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.di.AddNetworkMainComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.networkDetails.di.AddNetworkComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.di.ChainNetworkManagementComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.defaultNetworks.di.ExistingNetworkListComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.preConfiguredNetworks.di.PreConfiguredNetworksComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node.di.CustomNodeComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.settings.di.SettingsComponent
|
||||
import io.novafoundation.nova.feature_versions_api.di.VersionsFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.di.WalletConnectFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
SettingsFeatureDependencies::class,
|
||||
],
|
||||
modules = [
|
||||
SettingsFeatureModule::class,
|
||||
]
|
||||
)
|
||||
@FeatureScope
|
||||
interface SettingsFeatureComponent : SettingsFeatureApi {
|
||||
|
||||
fun settingsComponentFactory(): SettingsComponent.Factory
|
||||
|
||||
fun chainNetworkManagementFactory(): ChainNetworkManagementComponent.Factory
|
||||
|
||||
fun customNodeFactory(): CustomNodeComponent.Factory
|
||||
|
||||
fun networkManagementListFactory(): NetworkManagementListComponent.Factory
|
||||
|
||||
fun addNetworkMainFactory(): AddNetworkMainComponent.Factory
|
||||
|
||||
fun appearanceFactory(): AppearanceComponent.Factory
|
||||
|
||||
fun addNetworkFactory(): AddNetworkComponent.Factory
|
||||
|
||||
fun addedNetworkListFactory(): AddedNetworkListComponent.Factory
|
||||
|
||||
fun existingNetworkListFactory(): ExistingNetworkListComponent.Factory
|
||||
|
||||
fun preConfiguredNetworks(): PreConfiguredNetworksComponent.Factory
|
||||
|
||||
fun backupSettings(): CloudBackupSettingsComponent.Factory
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance router: SettingsRouter,
|
||||
@BindsInstance syncWalletsBackupPasswordCommunicator: SyncWalletsBackupPasswordCommunicator,
|
||||
@BindsInstance changeBackupPasswordCommunicator: ChangeBackupPasswordCommunicator,
|
||||
@BindsInstance restoreBackupPasswordCommunicator: RestoreBackupPasswordCommunicator,
|
||||
deps: SettingsFeatureDependencies
|
||||
): SettingsFeatureComponent
|
||||
}
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
CommonApi::class,
|
||||
RuntimeApi::class,
|
||||
AssetsFeatureApi::class,
|
||||
CurrencyFeatureApi::class,
|
||||
AccountFeatureApi::class,
|
||||
WalletConnectFeatureApi::class,
|
||||
VersionsFeatureApi::class,
|
||||
PushNotificationsFeatureApi::class,
|
||||
CloudBackupFeatureApi::class
|
||||
]
|
||||
)
|
||||
interface SettingsFeatureDependenciesComponent : SettingsFeatureDependencies
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.di
|
||||
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.data.network.AppLinksProvider
|
||||
import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser
|
||||
import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository
|
||||
import io.novafoundation.nova.common.data.repository.BannerVisibilityRepository
|
||||
import io.novafoundation.nova.common.domain.usecase.MaskingModeUseCase
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer
|
||||
import io.novafoundation.nova.common.resources.AppVersionProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.sequrity.SafeModeService
|
||||
import io.novafoundation.nova.common.sequrity.TwoFactorVerificationService
|
||||
import io.novafoundation.nova.common.sequrity.biometry.BiometricServiceFactory
|
||||
import io.novafoundation.nova.common.utils.progress.ProgressDialogMixinFactory
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncherFactory
|
||||
import io.novafoundation.nova.common.view.input.selector.ListSelectorMixin
|
||||
import io.novafoundation.nova.feature_account_api.data.cloudBackup.LocalAccountsCloudBackupFacade
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.MetaAccountTypePresentationMapper
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.addressActions.AddressActionsMixin
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.language.LanguageUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.CloudBackupService
|
||||
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
|
||||
import io.novafoundation.nova.feature_push_notifications.domain.interactor.PushNotificationsInteractor
|
||||
import io.novafoundation.nova.feature_push_notifications.domain.interactor.WelcomePushNotificationsInteractor
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
|
||||
import io.novafoundation.nova.runtime.ethereum.Web3ApiFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.healthState.NodeHealthStateTesterFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.RuntimeProviderPool
|
||||
import io.novafoundation.nova.runtime.repository.ChainNodeRepository
|
||||
import io.novafoundation.nova.runtime.repository.ChainRepository
|
||||
import io.novafoundation.nova.runtime.repository.PreConfiguredChainsRepository
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.CoinGeckoLinkValidationFactory
|
||||
|
||||
interface SettingsFeatureDependencies {
|
||||
|
||||
val maskingModeUseCase: MaskingModeUseCase
|
||||
|
||||
val cloudBackupService: CloudBackupService
|
||||
|
||||
val cloudBackupFacade: LocalAccountsCloudBackupFacade
|
||||
|
||||
val bannerVisRepository: BannerVisibilityRepository
|
||||
|
||||
val runtimeProviderPool: RuntimeProviderPool
|
||||
|
||||
val nodeHealthStateTesterFactory: NodeHealthStateTesterFactory
|
||||
|
||||
val chainNodeRepository: ChainNodeRepository
|
||||
|
||||
val nodeConnectionFactory: NodeConnectionFactory
|
||||
|
||||
val web3ApiFactory: Web3ApiFactory
|
||||
|
||||
val validationExecutor: ValidationExecutor
|
||||
|
||||
val preConfiguredChainsRepository: PreConfiguredChainsRepository
|
||||
|
||||
val coinGeckoLinkParser: CoinGeckoLinkParser
|
||||
|
||||
val chainRepository: ChainRepository
|
||||
|
||||
val coinGeckoLinkValidationFactory: CoinGeckoLinkValidationFactory
|
||||
|
||||
val assetsIconModeRepository: AssetsIconModeRepository
|
||||
|
||||
val accountRepository: AccountRepository
|
||||
|
||||
val accountInteractor: AccountInteractor
|
||||
|
||||
val chainRegistry: ChainRegistry
|
||||
|
||||
val languageUseCase: LanguageUseCase
|
||||
|
||||
val appLinksProvider: AppLinksProvider
|
||||
|
||||
val resourceManager: ResourceManager
|
||||
|
||||
val appVersionProvider: AppVersionProvider
|
||||
|
||||
val selectedAccountUseCase: SelectedAccountUseCase
|
||||
|
||||
val currencyInteractor: CurrencyInteractor
|
||||
|
||||
val safeModeService: SafeModeService
|
||||
|
||||
val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
|
||||
|
||||
val walletConnectSessionsUseCase: WalletConnectSessionsUseCase
|
||||
|
||||
val pushNotificationsInteractor: PushNotificationsInteractor
|
||||
|
||||
val welcomePushNotificationsInteractor: WelcomePushNotificationsInteractor
|
||||
|
||||
val imageLoader: ImageLoader
|
||||
|
||||
val metaAccountTypePresentationMapper: MetaAccountTypePresentationMapper
|
||||
|
||||
val addressIconGenerator: AddressIconGenerator
|
||||
|
||||
val walletUiUseCase: WalletUiUseCase
|
||||
|
||||
val extrinsicNavigationWrapper: ExtrinsicNavigationWrapper
|
||||
|
||||
val addressActionsMixinFactory: AddressActionsMixin.Factory
|
||||
|
||||
fun biometricServiceFactory(): BiometricServiceFactory
|
||||
|
||||
fun twoFactorVerificationService(): TwoFactorVerificationService
|
||||
|
||||
fun provideListSelectorMixinFactory(): ListSelectorMixin.Factory
|
||||
|
||||
fun actionBottomSheetLauncherFactory(): ActionBottomSheetLauncherFactory
|
||||
|
||||
fun progressDialogMixinFactory(): ProgressDialogMixinFactory
|
||||
|
||||
fun customDialogProvider(): CustomDialogDisplayer.Presentation
|
||||
|
||||
fun context(): Context
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.di
|
||||
|
||||
import io.novafoundation.nova.common.di.FeatureApiHolder
|
||||
import io.novafoundation.nova.common.di.FeatureContainer
|
||||
import io.novafoundation.nova.common.di.scope.ApplicationScope
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.ChangeBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.RestoreBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.createPassword.SyncWalletsBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_assets.di.AssetsFeatureApi
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.di.CloudBackupFeatureApi
|
||||
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
|
||||
import io.novafoundation.nova.feature_push_notifications.di.PushNotificationsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_versions_api.di.VersionsFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.di.WalletConnectFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
@ApplicationScope
|
||||
class SettingsFeatureHolder @Inject constructor(
|
||||
featureContainer: FeatureContainer,
|
||||
private val syncWalletsBackupPasswordCommunicator: SyncWalletsBackupPasswordCommunicator,
|
||||
private val changeBackupPasswordCommunicator: ChangeBackupPasswordCommunicator,
|
||||
private val restoreBackupPasswordCommunicator: RestoreBackupPasswordCommunicator,
|
||||
private val router: SettingsRouter,
|
||||
) : FeatureApiHolder(featureContainer) {
|
||||
|
||||
override fun initializeDependencies(): Any {
|
||||
val accountFeatureDependencies = DaggerSettingsFeatureComponent_SettingsFeatureDependenciesComponent.builder()
|
||||
.commonApi(commonApi())
|
||||
.runtimeApi(getFeature(RuntimeApi::class.java))
|
||||
.assetsFeatureApi(getFeature(AssetsFeatureApi::class.java))
|
||||
.currencyFeatureApi(getFeature(CurrencyFeatureApi::class.java))
|
||||
.versionsFeatureApi(getFeature(VersionsFeatureApi::class.java))
|
||||
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
|
||||
.walletConnectFeatureApi(getFeature(WalletConnectFeatureApi::class.java))
|
||||
.pushNotificationsFeatureApi(getFeature(PushNotificationsFeatureApi::class.java))
|
||||
.cloudBackupFeatureApi(getFeature(CloudBackupFeatureApi::class.java))
|
||||
.build()
|
||||
|
||||
return DaggerSettingsFeatureComponent.factory()
|
||||
.create(
|
||||
router = router,
|
||||
syncWalletsBackupPasswordCommunicator = syncWalletsBackupPasswordCommunicator,
|
||||
changeBackupPasswordCommunicator = changeBackupPasswordCommunicator,
|
||||
restoreBackupPasswordCommunicator = restoreBackupPasswordCommunicator,
|
||||
deps = accountFeatureDependencies
|
||||
)
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.di
|
||||
|
||||
import dagger.Module
|
||||
import io.novafoundation.nova.feature_account_api.data.cloudBackup.LocalAccountsCloudBackupFacade
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.CloudBackupService
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.CloudBackupSettingsInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.RealCloudBackupSettingsInteractor
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser
|
||||
import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository
|
||||
import io.novafoundation.nova.common.data.repository.BannerVisibilityRepository
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.CoinGeckoLinkValidationFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.data.NodeChainIdRepositoryFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.AddNetworkInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.AppearanceInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.CustomNodeInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.NetworkManagementChainInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.NetworkManagementInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.PreConfiguredNetworksInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.RealAddNetworkInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.RealAppearanceInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.RealCustomNodeInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.RealNetworkManagementChainInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.RealNetworkManagementInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.RealPreConfiguredNetworksInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.utils.CustomChainFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.NetworkListAdapterItemFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.RealNetworkListAdapterItemFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.Web3ApiFactory
|
||||
import io.novafoundation.nova.runtime.explorer.BlockExplorerLinkFormatter
|
||||
import io.novafoundation.nova.runtime.explorer.CommonBlockExplorerLinkFormatter
|
||||
import io.novafoundation.nova.runtime.explorer.EtherscanBlockExplorerLinkFormatter
|
||||
import io.novafoundation.nova.runtime.explorer.StatescanBlockExplorerLinkFormatter
|
||||
import io.novafoundation.nova.runtime.explorer.SubscanBlockExplorerLinkFormatter
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.healthState.NodeHealthStateTesterFactory
|
||||
import io.novafoundation.nova.runtime.repository.ChainNodeRepository
|
||||
import io.novafoundation.nova.runtime.repository.ChainRepository
|
||||
import io.novafoundation.nova.runtime.repository.PreConfiguredChainsRepository
|
||||
|
||||
@Module
|
||||
class SettingsFeatureModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideCloudBackupSettingsInteractor(
|
||||
accountRepository: AccountRepository,
|
||||
cloudBackupService: CloudBackupService,
|
||||
cloudBackupFacade: LocalAccountsCloudBackupFacade
|
||||
): CloudBackupSettingsInteractor {
|
||||
return RealCloudBackupSettingsInteractor(
|
||||
accountRepository,
|
||||
cloudBackupService,
|
||||
cloudBackupFacade
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNetworkManagementInteractor(
|
||||
chainRegistry: ChainRegistry,
|
||||
bannerVisRepository: BannerVisibilityRepository
|
||||
): NetworkManagementInteractor {
|
||||
return RealNetworkManagementInteractor(chainRegistry, bannerVisRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNetworkManagementChainInteractor(
|
||||
chainRegistry: ChainRegistry,
|
||||
nodeHealthStateTesterFactory: NodeHealthStateTesterFactory,
|
||||
chainRepository: ChainRepository,
|
||||
accountInteractor: AccountInteractor
|
||||
): NetworkManagementChainInteractor {
|
||||
return RealNetworkManagementChainInteractor(chainRegistry, nodeHealthStateTesterFactory, chainRepository, accountInteractor)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNetworkListAdapterItemFactory(
|
||||
resourceManager: ResourceManager
|
||||
): NetworkListAdapterItemFactory {
|
||||
return RealNetworkListAdapterItemFactory(resourceManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideNodeChainIdRepositoryFactory(
|
||||
nodeConnectionFactory: NodeConnectionFactory,
|
||||
web3ApiFactory: Web3ApiFactory
|
||||
): NodeChainIdRepositoryFactory {
|
||||
return NodeChainIdRepositoryFactory(nodeConnectionFactory, web3ApiFactory)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideCustomNodeInteractor(
|
||||
chainRegistry: ChainRegistry,
|
||||
chainNodeRepository: ChainNodeRepository,
|
||||
nodeChainIdRepositoryFactory: NodeChainIdRepositoryFactory,
|
||||
nodeConnectionFactory: NodeConnectionFactory
|
||||
): CustomNodeInteractor {
|
||||
return RealCustomNodeInteractor(
|
||||
chainRegistry,
|
||||
chainNodeRepository,
|
||||
nodeChainIdRepositoryFactory,
|
||||
nodeConnectionFactory
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providePreConfiguredNetworksInteractor(
|
||||
preConfiguredChainsRepository: PreConfiguredChainsRepository
|
||||
): PreConfiguredNetworksInteractor {
|
||||
return RealPreConfiguredNetworksInteractor(
|
||||
preConfiguredChainsRepository
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideBlockExplorerLinkFormatter(): BlockExplorerLinkFormatter {
|
||||
return CommonBlockExplorerLinkFormatter(
|
||||
listOf(
|
||||
SubscanBlockExplorerLinkFormatter(),
|
||||
StatescanBlockExplorerLinkFormatter(),
|
||||
EtherscanBlockExplorerLinkFormatter()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideCustomChainFactory(
|
||||
nodeConnectionFactory: NodeConnectionFactory,
|
||||
coinGeckoLinkParser: CoinGeckoLinkParser,
|
||||
blockExplorerLinkFormatter: BlockExplorerLinkFormatter,
|
||||
nodeChainIdRepositoryFactory: NodeChainIdRepositoryFactory
|
||||
): CustomChainFactory {
|
||||
return CustomChainFactory(
|
||||
nodeConnectionFactory,
|
||||
nodeChainIdRepositoryFactory,
|
||||
coinGeckoLinkParser,
|
||||
blockExplorerLinkFormatter
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAddNetworkInteractor(
|
||||
chainRepository: ChainRepository,
|
||||
chainRegistry: ChainRegistry,
|
||||
nodeChainIdRepositoryFactory: NodeChainIdRepositoryFactory,
|
||||
coinGeckoLinkValidationFactory: CoinGeckoLinkValidationFactory,
|
||||
coinGeckoLinkParser: CoinGeckoLinkParser,
|
||||
nodeConnectionFactory: NodeConnectionFactory,
|
||||
customChainFactory: CustomChainFactory
|
||||
): AddNetworkInteractor {
|
||||
return RealAddNetworkInteractor(
|
||||
chainRepository,
|
||||
chainRegistry,
|
||||
nodeChainIdRepositoryFactory,
|
||||
coinGeckoLinkValidationFactory,
|
||||
coinGeckoLinkParser,
|
||||
nodeConnectionFactory,
|
||||
customChainFactory
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAppearanceInteractor(assetsIconModeRepository: AssetsIconModeRepository): AppearanceInteractor {
|
||||
return RealAppearanceInteractor(assetsIconModeRepository)
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain
|
||||
|
||||
import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser
|
||||
import io.novafoundation.nova.common.data.network.runtime.model.firstTokenSymbol
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.CoinGeckoLinkValidationFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.data.NodeChainIdRepositoryFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.model.CustomNetworkPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.utils.CustomChainFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNetwork.CustomNetworkValidationSystem
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.NodeChainIdSingletonHelper
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.NodeConnectionSingletonHelper
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNetwork.validateTokenSymbol
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNetwork.validCoinGeckoLink
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNetwork.validateNetworkNodeIsAlive
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNetwork.validateNetworkNotAdded
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNetwork.validateNodeSupportedByNetwork
|
||||
import io.novafoundation.nova.runtime.ext.evmChainIdFrom
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.NetworkType
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory
|
||||
import io.novafoundation.nova.runtime.network.rpc.systemProperties
|
||||
import io.novafoundation.nova.runtime.repository.ChainRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface AddNetworkInteractor {
|
||||
|
||||
suspend fun createSubstrateNetwork(
|
||||
payload: CustomNetworkPayload,
|
||||
prefilledChain: Chain?,
|
||||
coroutineScope: CoroutineScope
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun createEvmNetwork(
|
||||
payload: CustomNetworkPayload,
|
||||
prefilledChain: Chain?
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun updateChain(
|
||||
chainId: String,
|
||||
chainName: String,
|
||||
tokenSymbol: String,
|
||||
blockExplorerModel: CustomNetworkPayload.BlockExplorer?,
|
||||
coingeckoLinkUrl: String?
|
||||
): Result<Unit>
|
||||
|
||||
fun getSubstrateValidationSystem(coroutineScope: CoroutineScope): CustomNetworkValidationSystem
|
||||
|
||||
fun getEvmValidationSystem(coroutineScope: CoroutineScope): CustomNetworkValidationSystem
|
||||
}
|
||||
|
||||
class RealAddNetworkInteractor(
|
||||
private val chainRepository: ChainRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val nodeChainIdRepositoryFactory: NodeChainIdRepositoryFactory,
|
||||
private val coinGeckoLinkValidationFactory: CoinGeckoLinkValidationFactory,
|
||||
private val coinGeckoLinkParser: CoinGeckoLinkParser,
|
||||
private val nodeConnectionFactory: NodeConnectionFactory,
|
||||
private val customChainFactory: CustomChainFactory
|
||||
) : AddNetworkInteractor {
|
||||
|
||||
override suspend fun createSubstrateNetwork(
|
||||
payload: CustomNetworkPayload,
|
||||
prefilledChain: Chain?,
|
||||
coroutineScope: CoroutineScope
|
||||
) = runCatching {
|
||||
val chain = customChainFactory.createSubstrateChain(payload, prefilledChain, coroutineScope)
|
||||
|
||||
chainRepository.addChain(chain)
|
||||
}
|
||||
|
||||
override suspend fun createEvmNetwork(
|
||||
payload: CustomNetworkPayload,
|
||||
prefilledChain: Chain?
|
||||
) = runCatching {
|
||||
val chain = customChainFactory.createEvmChain(payload, prefilledChain)
|
||||
|
||||
chainRepository.addChain(chain)
|
||||
}
|
||||
|
||||
override fun getSubstrateValidationSystem(coroutineScope: CoroutineScope): CustomNetworkValidationSystem {
|
||||
return ValidationSystem {
|
||||
validCoinGeckoLink(coinGeckoLinkValidationFactory)
|
||||
|
||||
// Using singleton her to receive chain id only one time for all vaildations
|
||||
val nodeConnectionHelper = getNodeConnectionSingletonHelper(coroutineScope)
|
||||
val chainIdHelper = getChainIdSingletonHelper(nodeConnectionHelper)
|
||||
validateNetworkNodeIsAlive { chainIdHelper.getChainId(NetworkType.SUBSTRATE, it.nodeUrl) }
|
||||
validateNetworkNotAdded(chainRegistry) { chainIdHelper.getChainId() }
|
||||
validateTokenSymbol {
|
||||
val systemProperties = nodeConnectionHelper.getNodeConnection()
|
||||
.getSocketService()
|
||||
.systemProperties()
|
||||
|
||||
systemProperties.firstTokenSymbol()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEvmValidationSystem(coroutineScope: CoroutineScope): CustomNetworkValidationSystem {
|
||||
return ValidationSystem {
|
||||
validCoinGeckoLink(coinGeckoLinkValidationFactory)
|
||||
|
||||
validateNetworkNotAdded(chainRegistry) { evmChainIdFrom(it.evmChainId!!) }
|
||||
|
||||
// Using singleton here to receive chain id only one time for all vaildations
|
||||
val nodeConnectionHelper = getNodeConnectionSingletonHelper(coroutineScope)
|
||||
val chainIdHelper = getChainIdSingletonHelper(nodeConnectionHelper)
|
||||
validateNetworkNodeIsAlive { chainIdHelper.getChainId(NetworkType.EVM, it.nodeUrl) }
|
||||
validateNodeSupportedByNetwork { chainIdHelper.getChainId() }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateChain(
|
||||
chainId: String,
|
||||
chainName: String,
|
||||
tokenSymbol: String,
|
||||
blockExplorerModel: CustomNetworkPayload.BlockExplorer?,
|
||||
coingeckoLinkUrl: String?
|
||||
): Result<Unit> {
|
||||
return runCatching {
|
||||
val blockExplorer = customChainFactory.getChainExplorer(blockExplorer = blockExplorerModel, chainId = chainId)
|
||||
val priceId = coingeckoLinkUrl?.let { coinGeckoLinkParser.parse(it).getOrNull()?.priceId }
|
||||
|
||||
chainRepository.editChain(chainId, chainName, tokenSymbol, blockExplorer, priceId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNodeConnectionSingletonHelper(coroutineScope: CoroutineScope): NodeConnectionSingletonHelper {
|
||||
return NodeConnectionSingletonHelper(nodeConnectionFactory, coroutineScope)
|
||||
}
|
||||
|
||||
private fun getChainIdSingletonHelper(helper: NodeConnectionSingletonHelper): NodeChainIdSingletonHelper {
|
||||
return NodeChainIdSingletonHelper(helper, nodeChainIdRepositoryFactory)
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain
|
||||
|
||||
import io.novafoundation.nova.common.data.model.AssetIconMode
|
||||
import io.novafoundation.nova.common.data.repository.AssetsIconModeRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AppearanceInteractor {
|
||||
|
||||
fun assetIconModeFlow(): Flow<AssetIconMode>
|
||||
|
||||
fun setIconMode(iconMode: AssetIconMode)
|
||||
}
|
||||
|
||||
class RealAppearanceInteractor(
|
||||
private val assetsIconModeRepository: AssetsIconModeRepository
|
||||
) : AppearanceInteractor {
|
||||
|
||||
override fun assetIconModeFlow() = assetsIconModeRepository.assetsIconModeFlow()
|
||||
|
||||
override fun setIconMode(iconMode: AssetIconMode) {
|
||||
assetsIconModeRepository.setAssetsIconMode(iconMode)
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain
|
||||
|
||||
import io.novafoundation.nova.common.list.GroupedList
|
||||
import io.novafoundation.nova.common.utils.finally
|
||||
import io.novafoundation.nova.common.utils.flatMap
|
||||
import io.novafoundation.nova.feature_account_api.data.cloudBackup.LocalAccountsCloudBackupFacade
|
||||
import io.novafoundation.nova.feature_account_api.data.cloudBackup.applyNonDestructiveCloudVersionOrThrow
|
||||
import io.novafoundation.nova.feature_account_api.data.cloudBackup.toMetaAccountType
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.metaAccountTypeComparator
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.CloudBackupService
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.fetchAndDecryptExistingBackupWithSavedPassword
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.WriteBackupRequest
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.CloudBackupDiff
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.isNotEmpty
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy.BackupDiffStrategy
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.strategy.BackupDiffStrategyFactory
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.setLastSyncedTimeAsNow
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.model.CloudBackupChangedAccount
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.model.CloudBackupChangedAccount.ChangingType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.Date
|
||||
|
||||
interface CloudBackupSettingsInteractor {
|
||||
|
||||
suspend fun isSyncCloudBackupEnabled(): Boolean
|
||||
|
||||
fun observeLastSyncedTime(): Flow<Date?>
|
||||
|
||||
suspend fun syncCloudBackup(): Result<Unit>
|
||||
|
||||
suspend fun setCloudBackupSyncEnabled(enable: Boolean)
|
||||
|
||||
suspend fun deleteCloudBackup(): Result<Unit>
|
||||
|
||||
suspend fun writeLocalBackupToCloud(): Result<Unit>
|
||||
|
||||
suspend fun signInToCloud(): Result<Unit>
|
||||
|
||||
fun prepareSortedLocalChangesFromDiff(cloudBackupDiff: CloudBackupDiff): GroupedList<LightMetaAccount.Type, CloudBackupChangedAccount>
|
||||
|
||||
suspend fun applyBackupAccountDiff(cloudBackupDiff: CloudBackupDiff, cloudBackup: CloudBackup): Result<Unit>
|
||||
}
|
||||
|
||||
class RealCloudBackupSettingsInteractor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val cloudBackupService: CloudBackupService,
|
||||
private val cloudBackupFacade: LocalAccountsCloudBackupFacade,
|
||||
) : CloudBackupSettingsInteractor {
|
||||
|
||||
override fun observeLastSyncedTime(): Flow<Date?> {
|
||||
return cloudBackupService.session.lastSyncedTimeFlow()
|
||||
}
|
||||
|
||||
override suspend fun syncCloudBackup(): Result<Unit> {
|
||||
return cloudBackupService.fetchAndDecryptExistingBackupWithSavedPassword()
|
||||
.mapCatching { cloudBackup ->
|
||||
cloudBackupFacade.applyNonDestructiveCloudVersionOrThrow(cloudBackup, getCloudBackupDiffStrategy())
|
||||
}.flatMap {
|
||||
if (it.cloudChanges.isNotEmpty()) {
|
||||
writeLocalBackupToCloud()
|
||||
} else {
|
||||
Result.success(Unit)
|
||||
}
|
||||
}.finally {
|
||||
cloudBackupService.session.setLastSyncedTimeAsNow()
|
||||
}.onSuccess {
|
||||
cloudBackupService.session.setBackupWasInitialized()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setCloudBackupSyncEnabled(enable: Boolean) {
|
||||
cloudBackupService.session.setSyncingBackupEnabled(enable)
|
||||
}
|
||||
|
||||
override suspend fun isSyncCloudBackupEnabled(): Boolean {
|
||||
return cloudBackupService.session.isSyncWithCloudEnabled()
|
||||
}
|
||||
|
||||
override suspend fun deleteCloudBackup(): Result<Unit> {
|
||||
return cloudBackupService.deleteBackup()
|
||||
}
|
||||
|
||||
override suspend fun writeLocalBackupToCloud(): Result<Unit> {
|
||||
return cloudBackupService.session.getSavedPassword()
|
||||
.flatMap { password ->
|
||||
val localSnapshot = cloudBackupFacade.fullBackupInfoFromLocalSnapshot()
|
||||
cloudBackupService.writeBackupToCloud(WriteBackupRequest(localSnapshot, password))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun signInToCloud(): Result<Unit> {
|
||||
return cloudBackupService.signInToCloud()
|
||||
}
|
||||
|
||||
override fun prepareSortedLocalChangesFromDiff(cloudBackupDiff: CloudBackupDiff): GroupedList<LightMetaAccount.Type, CloudBackupChangedAccount> {
|
||||
val accounts = localAccountChangesFromDiff(cloudBackupDiff.localChanges)
|
||||
.sortedBy { it.account.name }
|
||||
return accounts.groupBy { it.account.type.toMetaAccountType() }
|
||||
.toSortedMap(metaAccountTypeComparator())
|
||||
}
|
||||
|
||||
override suspend fun applyBackupAccountDiff(cloudBackupDiff: CloudBackupDiff, cloudBackup: CloudBackup): Result<Unit> {
|
||||
return runCatching {
|
||||
cloudBackupFacade.applyBackupDiff(cloudBackupDiff, cloudBackup)
|
||||
cloudBackupService.session.setLastSyncedTimeAsNow()
|
||||
selectMetaAccountIfNeeded()
|
||||
}.flatMap {
|
||||
if (cloudBackupDiff.cloudChanges.isNotEmpty()) {
|
||||
writeLocalBackupToCloud()
|
||||
} else {
|
||||
Result.success(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun localAccountChangesFromDiff(diff: CloudBackupDiff.PerSourceDiff): List<CloudBackupChangedAccount> {
|
||||
return diff.added.map { CloudBackupChangedAccount(ChangingType.ADDED, it) } +
|
||||
diff.modified.map { CloudBackupChangedAccount(ChangingType.CHANGED, it) } +
|
||||
diff.removed.map { CloudBackupChangedAccount(ChangingType.REMOVED, it) }
|
||||
}
|
||||
|
||||
private suspend fun selectMetaAccountIfNeeded() {
|
||||
if (!accountRepository.isAccountSelected()) {
|
||||
val metaAccounts = accountRepository.getActiveMetaAccounts()
|
||||
if (metaAccounts.isNotEmpty()) {
|
||||
accountRepository.selectMetaAccount(metaAccounts.first().id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCloudBackupDiffStrategy(): BackupDiffStrategyFactory {
|
||||
return if (cloudBackupService.session.cloudBackupWasInitialized()) {
|
||||
BackupDiffStrategy.syncWithCloud()
|
||||
} else {
|
||||
BackupDiffStrategy.importFromCloud()
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_settings_impl.data.NodeChainIdRepositoryFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.NodeChainIdSingletonHelper
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.NodeConnectionSingletonHelper
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNode.NetworkNodeValidationSystem
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNode.validateNetworkNodeIsAlive
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNode.validateNodeNotAdded
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNode.validateNodeSupportedByNetwork
|
||||
import io.novafoundation.nova.runtime.ext.networkType
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory
|
||||
import io.novafoundation.nova.runtime.repository.ChainNodeRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface CustomNodeInteractor {
|
||||
|
||||
suspend fun getNodeDetails(chainId: String, nodeUrl: String): Result<Chain.Node>
|
||||
|
||||
suspend fun createNode(chainId: String, url: String, name: String)
|
||||
|
||||
suspend fun updateNode(chainId: String, oldUrl: String, url: String, name: String)
|
||||
|
||||
fun getValidationSystem(coroutineScope: CoroutineScope, skipNodeExistValidation: Boolean): NetworkNodeValidationSystem
|
||||
}
|
||||
|
||||
class RealCustomNodeInteractor(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val chainNodeRepository: ChainNodeRepository,
|
||||
private val nodeChainIdRepositoryFactory: NodeChainIdRepositoryFactory,
|
||||
private val nodeConnectionFactory: NodeConnectionFactory
|
||||
) : CustomNodeInteractor {
|
||||
|
||||
override suspend fun getNodeDetails(chainId: String, nodeUrl: String): Result<Chain.Node> {
|
||||
return runCatching {
|
||||
chainRegistry.getChain(chainId).nodes
|
||||
.nodes
|
||||
.first { it.unformattedUrl == nodeUrl }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createNode(chainId: String, url: String, name: String) {
|
||||
chainNodeRepository.createChainNode(chainId, url, name)
|
||||
}
|
||||
|
||||
override suspend fun updateNode(chainId: String, oldUrl: String, url: String, name: String) {
|
||||
chainNodeRepository.saveChainNode(chainId, oldUrl, url, name)
|
||||
}
|
||||
|
||||
override fun getValidationSystem(coroutineScope: CoroutineScope, skipNodeExistValidation: Boolean): NetworkNodeValidationSystem {
|
||||
return ValidationSystem {
|
||||
if (!skipNodeExistValidation) {
|
||||
validateNodeNotAdded()
|
||||
}
|
||||
|
||||
val nodeHelper = NodeConnectionSingletonHelper(nodeConnectionFactory, coroutineScope)
|
||||
val chainIdRequestSingleton = NodeChainIdSingletonHelper(nodeHelper, nodeChainIdRepositoryFactory)
|
||||
|
||||
validateNetworkNodeIsAlive { chainIdRequestSingleton.getChainId(it.chain.networkType(), it.nodeUrl) }
|
||||
|
||||
validateNodeSupportedByNetwork { chainIdRequestSingleton.getChainId() }
|
||||
}
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain
|
||||
|
||||
import io.novafoundation.nova.common.utils.combine
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.runtime.ext.Geneses
|
||||
import io.novafoundation.nova.runtime.ext.genesisHash
|
||||
import io.novafoundation.nova.runtime.ext.isCustomNetwork
|
||||
import io.novafoundation.nova.runtime.ext.isDisabled
|
||||
import io.novafoundation.nova.runtime.ext.isEnabled
|
||||
import io.novafoundation.nova.runtime.ext.selectedUnformattedWssNodeUrlOrNull
|
||||
import io.novafoundation.nova.runtime.ext.wssNodes
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Nodes.NodeSelectionStrategy
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.healthState.NodeHealthStateTesterFactory
|
||||
import io.novafoundation.nova.runtime.repository.ChainRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ChainNetworkState(
|
||||
val chain: Chain,
|
||||
val networkCanBeDisabled: Boolean,
|
||||
val nodeHealthStates: List<NodeHealthState>,
|
||||
val connectingNode: Chain.Node?
|
||||
)
|
||||
|
||||
class NodeHealthState(
|
||||
val node: Chain.Node,
|
||||
val state: State
|
||||
) {
|
||||
|
||||
sealed interface State {
|
||||
|
||||
object Connecting : State
|
||||
|
||||
class Connected(val ms: Long) : State
|
||||
|
||||
object Disabled : State
|
||||
}
|
||||
}
|
||||
|
||||
interface NetworkManagementChainInteractor {
|
||||
|
||||
fun chainStateFlow(chainId: String, coroutineScope: CoroutineScope): Flow<ChainNetworkState>
|
||||
|
||||
suspend fun toggleAutoBalance(chainId: String)
|
||||
|
||||
suspend fun selectNode(chainId: String, unformattedNodeUrl: String)
|
||||
|
||||
suspend fun toggleChainEnableState(chainId: String)
|
||||
|
||||
suspend fun deleteNetwork(chainId: String)
|
||||
|
||||
suspend fun deleteNode(chainId: String, unformattedNodeUrl: String)
|
||||
}
|
||||
|
||||
class RealNetworkManagementChainInteractor(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val nodeHealthStateTesterFactory: NodeHealthStateTesterFactory,
|
||||
private val chainRepository: ChainRepository,
|
||||
private val accountInteractor: AccountInteractor
|
||||
) : NetworkManagementChainInteractor {
|
||||
|
||||
override fun chainStateFlow(chainId: String, coroutineScope: CoroutineScope): Flow<ChainNetworkState> {
|
||||
return chainRegistry.chainsById
|
||||
.mapNotNull { it[chainId] }
|
||||
.flatMapLatest { chain ->
|
||||
combine(activeNodeFlow(chainId), nodesHealthState(chain, coroutineScope)) { activeNode, nodeHealthStates ->
|
||||
ChainNetworkState(chain, networkCanBeDisabled(chain), nodeHealthStates, activeNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun toggleAutoBalance(chainId: String) {
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
chainRegistry.setWssNodeSelectionStrategy(chainId, chain.nodes.strategyForToggledWssAutoBalance())
|
||||
}
|
||||
|
||||
private fun Chain.Nodes.strategyForToggledWssAutoBalance(): NodeSelectionStrategy {
|
||||
return when (wssNodeSelectionStrategy) {
|
||||
NodeSelectionStrategy.AutoBalance -> {
|
||||
val firstNode = wssNodes().first()
|
||||
NodeSelectionStrategy.SelectedNode(firstNode.unformattedUrl)
|
||||
}
|
||||
|
||||
is NodeSelectionStrategy.SelectedNode -> NodeSelectionStrategy.AutoBalance
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun selectNode(chainId: String, unformattedNodeUrl: String) {
|
||||
val strategy = NodeSelectionStrategy.SelectedNode(unformattedNodeUrl)
|
||||
chainRegistry.setWssNodeSelectionStrategy(chainId, strategy)
|
||||
}
|
||||
|
||||
override suspend fun toggleChainEnableState(chainId: String) {
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
val connectionState = if (chain.isEnabled) Chain.ConnectionState.DISABLED else Chain.ConnectionState.FULL_SYNC
|
||||
chainRegistry.changeChainConnectionState(chainId, connectionState)
|
||||
}
|
||||
|
||||
override suspend fun deleteNetwork(chainId: String) {
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
|
||||
require(chain.isCustomNetwork)
|
||||
|
||||
withContext(Dispatchers.Default) { accountInteractor.deleteProxiedMetaAccountsByChain(chainId) } // Delete proxied meta accounts manually
|
||||
chainRepository.deleteNetwork(chainId)
|
||||
}
|
||||
|
||||
override suspend fun deleteNode(chainId: String, unformattedNodeUrl: String) {
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
|
||||
require(chain.nodes.nodes.size > 1)
|
||||
|
||||
chainRepository.deleteNode(chainId, unformattedNodeUrl)
|
||||
|
||||
if (chain.selectedUnformattedWssNodeUrlOrNull == unformattedNodeUrl) {
|
||||
chainRegistry.setWssNodeSelectionStrategy(chainId, NodeSelectionStrategy.AutoBalance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun networkCanBeDisabled(chain: Chain): Boolean {
|
||||
return chain.genesisHash != Chain.Geneses.POLKADOT
|
||||
}
|
||||
|
||||
private fun nodesHealthState(chain: Chain, coroutineScope: CoroutineScope): Flow<List<NodeHealthState>> {
|
||||
return chain.nodes.wssNodes().map {
|
||||
nodeHealthState(chain, it, coroutineScope)
|
||||
}.combine()
|
||||
}
|
||||
|
||||
private fun activeNodeFlow(chainId: String): Flow<Chain.Node?> {
|
||||
val activeConnection = chainRegistry.getConnectionOrNull(chainId)
|
||||
return activeConnection?.currentUrl?.map { it?.node } ?: flowOf { null }
|
||||
}
|
||||
|
||||
private fun nodeHealthState(chain: Chain, node: Chain.Node, coroutineScope: CoroutineScope): Flow<NodeHealthState> {
|
||||
return flow {
|
||||
if (chain.isDisabled) {
|
||||
emit(NodeHealthState(node, NodeHealthState.State.Disabled))
|
||||
return@flow
|
||||
}
|
||||
|
||||
emit(NodeHealthState(node, NodeHealthState.State.Connecting))
|
||||
|
||||
val nodeConnectionDelay = nodeHealthStateTesterFactory.create(chain, node, coroutineScope)
|
||||
.testNodeHealthState()
|
||||
.getOrNull()
|
||||
|
||||
nodeConnectionDelay?.let {
|
||||
emit(NodeHealthState(node, NodeHealthState.State.Connected(it)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain
|
||||
|
||||
import io.novafoundation.nova.common.data.repository.BannerVisibilityRepository
|
||||
import io.novafoundation.nova.common.utils.filterList
|
||||
import io.novafoundation.nova.common.utils.combine
|
||||
import io.novafoundation.nova.runtime.ext.defaultComparatorFrom
|
||||
import io.novafoundation.nova.runtime.ext.isCustomNetwork
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.wsrpc.state.SocketStateMachine
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private const val INTEGRATE_NETWORKS_BANNER_TAG = "INTEGRATE_NETWORKS_BANNER_TAG"
|
||||
|
||||
class NetworkState(
|
||||
val chain: Chain,
|
||||
val connectionState: SocketStateMachine.State?
|
||||
)
|
||||
|
||||
interface NetworkManagementInteractor {
|
||||
|
||||
fun shouldShowBanner(): Flow<Boolean>
|
||||
|
||||
suspend fun hideBanner()
|
||||
|
||||
fun defaultNetworksFlow(): Flow<List<NetworkState>>
|
||||
|
||||
fun addedNetworksFlow(): Flow<List<NetworkState>>
|
||||
}
|
||||
|
||||
class RealNetworkManagementInteractor(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val bannerVisibilityRepository: BannerVisibilityRepository
|
||||
) : NetworkManagementInteractor {
|
||||
|
||||
override fun shouldShowBanner(): Flow<Boolean> {
|
||||
return bannerVisibilityRepository.shouldShowBannerFlow(INTEGRATE_NETWORKS_BANNER_TAG)
|
||||
}
|
||||
|
||||
override suspend fun hideBanner() {
|
||||
bannerVisibilityRepository.hideBanner(INTEGRATE_NETWORKS_BANNER_TAG)
|
||||
}
|
||||
|
||||
override fun defaultNetworksFlow(): Flow<List<NetworkState>> {
|
||||
return networksFlow { !it.isCustomNetwork }
|
||||
}
|
||||
|
||||
override fun addedNetworksFlow(): Flow<List<NetworkState>> {
|
||||
return networksFlow { it.isCustomNetwork }
|
||||
}
|
||||
|
||||
private fun networksFlow(filter: (Chain) -> Boolean) = chainRegistry.currentChains
|
||||
.filterList(filter)
|
||||
.flatMapLatest { chains ->
|
||||
connectionsFlow(sortChains(chains))
|
||||
}
|
||||
|
||||
private fun connectionsFlow(chains: List<Chain>): Flow<List<NetworkState>> {
|
||||
if (chains.isEmpty()) {
|
||||
return flowOf(emptyList())
|
||||
}
|
||||
|
||||
return chains.map { chain ->
|
||||
val connectionFlow = chainRegistry.getConnectionOrNull(chain.id)?.state ?: flowOf<SocketStateMachine.State?>(SocketStateMachine.State.Disconnected)
|
||||
connectionFlow
|
||||
.distinctUntilChanged { old, new -> old?.isConnected() == new?.isConnected() }
|
||||
.map { state -> NetworkState(chain, state) }
|
||||
}.combine()
|
||||
}
|
||||
|
||||
private fun sortChains(chains: List<Chain>): List<Chain> {
|
||||
return chains.sortedWith(Chain.defaultComparatorFrom { it })
|
||||
}
|
||||
|
||||
// It's enough for us to have 2 states for this implementation: connected and not connected
|
||||
private fun SocketStateMachine.State.isConnected(): Boolean {
|
||||
return this is SocketStateMachine.State.Connected
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.LightChain
|
||||
import io.novafoundation.nova.runtime.repository.PreConfiguredChainsRepository
|
||||
|
||||
interface PreConfiguredNetworksInteractor {
|
||||
|
||||
suspend fun getPreConfiguredNetworks(): Result<List<LightChain>>
|
||||
|
||||
suspend fun excludeChains(list: List<LightChain>, chainIds: Set<String>): List<LightChain>
|
||||
|
||||
fun searchNetworks(query: String?, list: List<LightChain>): List<LightChain>
|
||||
|
||||
suspend fun getPreConfiguredNetwork(chainId: String): Result<Chain>
|
||||
}
|
||||
|
||||
class RealPreConfiguredNetworksInteractor(
|
||||
private val preConfiguredChainsRepository: PreConfiguredChainsRepository
|
||||
) : PreConfiguredNetworksInteractor {
|
||||
|
||||
override suspend fun getPreConfiguredNetworks(): Result<List<LightChain>> {
|
||||
return preConfiguredChainsRepository.getPreConfiguredChains()
|
||||
.map { lightChains ->
|
||||
lightChains.sortedBy { it.name }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun excludeChains(list: List<LightChain>, chainIds: Set<String>): List<LightChain> {
|
||||
return list.filterNot { lightChain -> chainIds.contains(lightChain.id) }
|
||||
}
|
||||
|
||||
override fun searchNetworks(query: String?, list: List<LightChain>): List<LightChain> {
|
||||
if (query.isNullOrBlank()) return list
|
||||
|
||||
val loverCaseQuery = query.trim().lowercase()
|
||||
|
||||
return list.filter { lightChain -> lightChain.name.lowercase().startsWith(loverCaseQuery) }
|
||||
}
|
||||
|
||||
override suspend fun getPreConfiguredNetwork(chainId: String): Result<Chain> {
|
||||
return preConfiguredChainsRepository.getPreconfiguredChainById(chainId)
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.model
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
|
||||
|
||||
class CloudBackupChangedAccount(val changingType: ChangingType, val account: CloudBackup.WalletPublicInfo) {
|
||||
|
||||
enum class ChangingType {
|
||||
ADDED,
|
||||
REMOVED,
|
||||
CHANGED
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.model
|
||||
|
||||
data class CustomNetworkPayload(
|
||||
val nodeUrl: String,
|
||||
val nodeName: String,
|
||||
val chainName: String,
|
||||
val tokenSymbol: String,
|
||||
val evmChainId: Int?,
|
||||
val blockExplorer: BlockExplorer?,
|
||||
val coingeckoLinkUrl: String?,
|
||||
val ignoreChainModifying: Boolean,
|
||||
) {
|
||||
|
||||
class BlockExplorer(
|
||||
val name: String,
|
||||
val url: String
|
||||
)
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.utils
|
||||
|
||||
import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser
|
||||
import io.novafoundation.nova.common.data.network.runtime.model.SystemProperties
|
||||
import io.novafoundation.nova.common.data.network.runtime.model.firstTokenDecimals
|
||||
import io.novafoundation.nova.common.utils.DEFAULT_PREFIX
|
||||
import io.novafoundation.nova.common.utils.Precision
|
||||
import io.novafoundation.nova.common.utils.asPrecision
|
||||
import io.novafoundation.nova.common.utils.asTokenSymbol
|
||||
import io.novafoundation.nova.common.utils.orFalse
|
||||
import io.novafoundation.nova.feature_settings_impl.data.NodeChainIdRepositoryFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.model.CustomNetworkPayload
|
||||
import io.novafoundation.nova.runtime.explorer.BlockExplorerLinkFormatter
|
||||
import io.novafoundation.nova.runtime.ext.EVM_DEFAULT_TOKEN_DECIMALS
|
||||
import io.novafoundation.nova.runtime.ext.evmChainIdFrom
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Nodes.NodeSelectionStrategy
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnection
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory
|
||||
import io.novafoundation.nova.runtime.network.rpc.systemProperties
|
||||
import io.novafoundation.nova.runtime.util.fetchRuntimeSnapshot
|
||||
import io.novafoundation.nova.runtime.util.isEthereumAddress
|
||||
import io.novasama.substrate_sdk_android.ss58.SS58Encoder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class CustomChainFactory(
|
||||
private val nodeConnectionFactory: NodeConnectionFactory,
|
||||
private val nodeChainIdRepositoryFactory: NodeChainIdRepositoryFactory,
|
||||
private val coinGeckoLinkParser: CoinGeckoLinkParser,
|
||||
private val blockExplorerLinkFormatter: BlockExplorerLinkFormatter,
|
||||
) {
|
||||
|
||||
suspend fun createSubstrateChain(
|
||||
payload: CustomNetworkPayload,
|
||||
prefilledChain: Chain?,
|
||||
coroutineScope: CoroutineScope
|
||||
): Chain {
|
||||
val nodeConnection = nodeConnectionFactory.createNodeConnection(payload.nodeUrl, coroutineScope)
|
||||
val substrateNodeIdRequester = nodeChainIdRepositoryFactory.substrate(nodeConnection)
|
||||
val runtime = nodeConnection.getSocketService().fetchRuntimeSnapshot()
|
||||
|
||||
val (precision, addressPrefix) = getMainTokenPrecisionAndAddressPrefix(prefilledChain, nodeConnection)
|
||||
|
||||
return createChain(
|
||||
chainId = substrateNodeIdRequester.requestChainId(),
|
||||
addressPrefix = addressPrefix,
|
||||
isEthereumBased = runtime.isEthereumAddress(),
|
||||
hasSubstrateRuntime = true,
|
||||
assetDecimals = precision,
|
||||
assetType = prefilledChain?.utilityAsset?.type ?: Chain.Asset.Type.Native,
|
||||
payload = payload,
|
||||
prefilledChain = prefilledChain,
|
||||
)
|
||||
}
|
||||
|
||||
fun createEvmChain(
|
||||
payload: CustomNetworkPayload,
|
||||
prefilledChain: Chain?
|
||||
): Chain {
|
||||
val evmChainId = payload.evmChainId!!
|
||||
val chainId = evmChainIdFrom(evmChainId)
|
||||
|
||||
return createChain(
|
||||
chainId = chainId,
|
||||
addressPrefix = evmChainId,
|
||||
isEthereumBased = true,
|
||||
hasSubstrateRuntime = false,
|
||||
assetDecimals = EVM_DEFAULT_TOKEN_DECIMALS.asPrecision(),
|
||||
assetType = Chain.Asset.Type.EvmNative,
|
||||
payload = payload,
|
||||
prefilledChain = prefilledChain,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createChain(
|
||||
chainId: String,
|
||||
addressPrefix: Int,
|
||||
isEthereumBased: Boolean,
|
||||
hasSubstrateRuntime: Boolean,
|
||||
assetDecimals: Precision,
|
||||
assetType: Chain.Asset.Type,
|
||||
payload: CustomNetworkPayload,
|
||||
prefilledChain: Chain?
|
||||
): Chain {
|
||||
val priceId = payload.coingeckoLinkUrl?.let { coinGeckoLinkParser.parse(it).getOrNull()?.priceId }
|
||||
|
||||
val prefilledUtilityAsset = prefilledChain?.utilityAsset
|
||||
|
||||
val asset = Chain.Asset(
|
||||
id = 0,
|
||||
name = payload.chainName,
|
||||
enabled = true,
|
||||
icon = prefilledUtilityAsset?.icon,
|
||||
priceId = priceId,
|
||||
chainId = chainId,
|
||||
symbol = payload.tokenSymbol.asTokenSymbol(),
|
||||
precision = assetDecimals,
|
||||
buyProviders = prefilledUtilityAsset?.buyProviders.orEmpty(),
|
||||
sellProviders = prefilledUtilityAsset?.sellProviders.orEmpty(),
|
||||
staking = prefilledUtilityAsset?.staking.orEmpty(),
|
||||
type = assetType,
|
||||
source = Chain.Asset.Source.MANUAL,
|
||||
)
|
||||
|
||||
val explorer = getChainExplorer(payload.blockExplorer, chainId)
|
||||
|
||||
val nodes = Chain.Nodes(
|
||||
autoBalanceStrategy = prefilledChain?.nodes?.autoBalanceStrategy ?: Chain.Nodes.AutoBalanceStrategy.ROUND_ROBIN,
|
||||
wssNodeSelectionStrategy = NodeSelectionStrategy.AutoBalance,
|
||||
nodes = createNodeList(chainId, prefilledChain, payload)
|
||||
)
|
||||
|
||||
return Chain(
|
||||
id = chainId,
|
||||
parentId = prefilledChain?.parentId,
|
||||
name = payload.chainName,
|
||||
assets = listOf(asset),
|
||||
nodes = nodes,
|
||||
explorers = explorer?.let(::listOf) ?: prefilledChain?.explorers.orEmpty(),
|
||||
externalApis = prefilledChain?.externalApis.orEmpty(),
|
||||
icon = prefilledChain?.icon,
|
||||
addressPrefix = addressPrefix,
|
||||
legacyAddressPrefix = null,
|
||||
types = prefilledChain?.types,
|
||||
isEthereumBased = isEthereumBased,
|
||||
isTestNet = prefilledChain?.isTestNet.orFalse(),
|
||||
source = Chain.Source.CUSTOM,
|
||||
hasSubstrateRuntime = hasSubstrateRuntime,
|
||||
pushSupport = prefilledChain?.pushSupport.orFalse(),
|
||||
hasCrowdloans = prefilledChain?.hasCrowdloans.orFalse(),
|
||||
multisigSupport = prefilledChain?.multisigSupport.orFalse(),
|
||||
supportProxy = prefilledChain?.supportProxy.orFalse(),
|
||||
governance = prefilledChain?.governance.orEmpty(),
|
||||
swap = prefilledChain?.swap.orEmpty(),
|
||||
customFee = prefilledChain?.customFee.orEmpty(),
|
||||
connectionState = Chain.ConnectionState.FULL_SYNC,
|
||||
additional = prefilledChain?.additional
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNodeList(
|
||||
chainId: String,
|
||||
prefilledChain: Chain?,
|
||||
input: CustomNetworkPayload
|
||||
): List<Chain.Node> {
|
||||
val inputNode = Chain.Node(
|
||||
chainId = chainId,
|
||||
unformattedUrl = input.nodeUrl,
|
||||
name = input.nodeName,
|
||||
orderId = 0,
|
||||
isCustom = true,
|
||||
)
|
||||
|
||||
val prefilledNodes = prefilledChain?.nodes?.nodes.orEmpty()
|
||||
val prefilledExceptInput = prefilledNodes.mapNotNull {
|
||||
val differentFromInput = it.unformattedUrl != inputNode.unformattedUrl
|
||||
|
||||
if (differentFromInput) {
|
||||
// Consider prefilled nodes as custom
|
||||
it.copy(isCustom = true)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return buildList {
|
||||
add(inputNode)
|
||||
addAll(prefilledExceptInput)
|
||||
}
|
||||
}
|
||||
|
||||
fun getChainExplorer(blockExplorer: CustomNetworkPayload.BlockExplorer?, chainId: String): Chain.Explorer? {
|
||||
return blockExplorer?.let {
|
||||
val links = blockExplorerLinkFormatter.format(it.url)
|
||||
Chain.Explorer(
|
||||
chainId = chainId,
|
||||
name = it.name,
|
||||
account = links?.account,
|
||||
extrinsic = links?.extrinsic,
|
||||
event = links?.event,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSubstrateChainProperties(nodeConnection: NodeConnection): SystemProperties {
|
||||
return nodeConnection.getSocketService().systemProperties()
|
||||
}
|
||||
|
||||
private suspend fun getMainTokenPrecisionAndAddressPrefix(chain: Chain?, nodeConnection: NodeConnection): Pair<Precision, Int> {
|
||||
if (chain != null) {
|
||||
val asset = chain.utilityAsset
|
||||
return Pair(asset.precision, chain.addressPrefix)
|
||||
} else {
|
||||
val systemProperties = getSubstrateChainProperties(nodeConnection)
|
||||
return Pair(
|
||||
systemProperties.firstTokenDecimals().asPrecision(),
|
||||
systemProperties.ss58Format ?: systemProperties.SS58Prefix ?: SS58Encoder.DEFAULT_PREFIX.toInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.validation
|
||||
|
||||
import io.novafoundation.nova.feature_settings_impl.data.NodeChainIdRepositoryFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.NetworkType
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class NodeChainIdSingletonHelper(
|
||||
private val nodeConnectionSingletonHelper: NodeConnectionSingletonHelper,
|
||||
private val nodeChainIdRepositoryFactory: NodeChainIdRepositoryFactory
|
||||
) {
|
||||
|
||||
private var chainId: String? = null
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun getChainId(networkType: NetworkType, nodeUrl: String): String {
|
||||
return mutex.withLock {
|
||||
if (chainId == null) {
|
||||
val nodeConnection = nodeConnectionSingletonHelper.getNodeConnection(nodeUrl)
|
||||
val nodeChainIdRepository = nodeChainIdRepositoryFactory.create(networkType, nodeConnection)
|
||||
chainId = nodeChainIdRepository.requestChainId()
|
||||
chainId!!
|
||||
} else {
|
||||
chainId!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getChainId(): String {
|
||||
return chainId!!
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.validation
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnection
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class NodeConnectionSingletonHelper(
|
||||
private val nodeConnectionFactory: NodeConnectionFactory,
|
||||
private val coroutineScope: CoroutineScope
|
||||
) {
|
||||
|
||||
private var nodeConnection: NodeConnection? = null
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun getNodeConnection(nodeUrl: String): NodeConnection {
|
||||
return mutex.withLock {
|
||||
if (nodeConnection == null) {
|
||||
nodeConnection = nodeConnectionFactory.createNodeConnection(nodeUrl, coroutineScope)
|
||||
nodeConnection!!
|
||||
} else {
|
||||
nodeConnection!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getNodeConnection(): NodeConnection {
|
||||
return nodeConnection!!
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.validation.customNetwork
|
||||
|
||||
import io.novafoundation.nova.common.validation.Validation
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
|
||||
import io.novafoundation.nova.common.validation.validOrError
|
||||
|
||||
class CustomNetworkAssetValidation<P, F>(
|
||||
private val chainMainAssetSymbolRequester: suspend (P) -> String,
|
||||
private val symbol: (P) -> String,
|
||||
private val failure: (P, String) -> F
|
||||
) : Validation<P, F> {
|
||||
|
||||
override suspend fun validate(value: P): ValidationStatus<F> {
|
||||
val networkSymbol = chainMainAssetSymbolRequester(value)
|
||||
|
||||
return validOrError(networkSymbol == symbol(value)) {
|
||||
failure(value, networkSymbol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <P, F> ValidationSystemBuilder<P, F>.validateAssetIsMain(
|
||||
chainMainAssetSymbolRequester: suspend (P) -> String,
|
||||
symbol: (P) -> String,
|
||||
failure: (P, String) -> F
|
||||
) = validate(
|
||||
CustomNetworkAssetValidation(chainMainAssetSymbolRequester, symbol, failure)
|
||||
)
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.validation.customNetwork
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.CoinGeckoLinkValidationFactory
|
||||
import io.novafoundation.nova.feature_assets.domain.tokens.add.validations.validCoinGeckoLink
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.model.CustomNetworkPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNode.nodeSupportedByNetworkValidation
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNode.validateNetworkNodeIsAlive
|
||||
import io.novafoundation.nova.runtime.ext.evmChainIdFrom
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
typealias CustomNetworkValidationSystem = ValidationSystem<CustomNetworkPayload, CustomNetworkFailure>
|
||||
typealias CustomNetworkValidationSystemBuilder = ValidationSystemBuilder<CustomNetworkPayload, CustomNetworkFailure>
|
||||
|
||||
sealed interface CustomNetworkFailure {
|
||||
|
||||
class DefaultNetworkAlreadyAdded(val networkName: String) : CustomNetworkFailure
|
||||
|
||||
class CustomNetworkAlreadyAdded(val networkName: String) : CustomNetworkFailure
|
||||
|
||||
object WrongNetwork : CustomNetworkFailure
|
||||
|
||||
object NodeIsNotAlive : CustomNetworkFailure
|
||||
|
||||
object CoingeckoLinkBadFormat : CustomNetworkFailure
|
||||
|
||||
class WrongAsset(val usedSymbol: String, val correctSymbol: String) : CustomNetworkFailure
|
||||
}
|
||||
|
||||
fun CustomNetworkValidationSystemBuilder.validateNetworkNodeIsAlive(
|
||||
nodeHealthStateCheckRequest: suspend (CustomNetworkPayload) -> Unit
|
||||
) = validateNetworkNodeIsAlive(
|
||||
nodeHealthStateCheckRequest,
|
||||
nodeUrl = { it.nodeUrl },
|
||||
failure = { CustomNetworkFailure.NodeIsNotAlive }
|
||||
)
|
||||
|
||||
fun CustomNetworkValidationSystemBuilder.validateNodeSupportedByNetwork(
|
||||
nodeChainIdRequester: suspend (CustomNetworkPayload) -> String
|
||||
) = nodeSupportedByNetworkValidation(
|
||||
nodeChainIdRequester = { nodeChainIdRequester(it) },
|
||||
originalChainId = { it.evmChainId?.let { evmChainIdFrom(it) } },
|
||||
failure = { CustomNetworkFailure.WrongNetwork }
|
||||
)
|
||||
|
||||
fun CustomNetworkValidationSystemBuilder.validateNetworkNotAdded(
|
||||
chainRegistry: ChainRegistry,
|
||||
chainIdRequester: suspend (CustomNetworkPayload) -> String
|
||||
) = validateNetworkNotAdded(
|
||||
chainRegistry = chainRegistry,
|
||||
chainIdRequester = { chainIdRequester(it) },
|
||||
ignoreChainModifying = { it.ignoreChainModifying },
|
||||
defaultNetworkFailure = { payload, chain -> CustomNetworkFailure.DefaultNetworkAlreadyAdded(chain.name) },
|
||||
customNetworkFailure = { payload, chain -> CustomNetworkFailure.CustomNetworkAlreadyAdded(chain.name) }
|
||||
)
|
||||
|
||||
fun CustomNetworkValidationSystemBuilder.validCoinGeckoLink(
|
||||
coinGeckoLinkValidationFactory: CoinGeckoLinkValidationFactory
|
||||
) = validCoinGeckoLink(
|
||||
coinGeckoLinkValidationFactory = coinGeckoLinkValidationFactory,
|
||||
optional = true,
|
||||
link = { it.coingeckoLinkUrl },
|
||||
error = { CustomNetworkFailure.CoingeckoLinkBadFormat }
|
||||
)
|
||||
|
||||
fun CustomNetworkValidationSystemBuilder.validateTokenSymbol(
|
||||
tokenSymbolRequester: suspend (CustomNetworkPayload) -> String
|
||||
) = validateAssetIsMain(
|
||||
chainMainAssetSymbolRequester = tokenSymbolRequester,
|
||||
symbol = { it.tokenSymbol },
|
||||
failure = { payload, correctSymbol -> CustomNetworkFailure.WrongAsset(payload.tokenSymbol, correctSymbol) },
|
||||
)
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.validation.customNetwork
|
||||
|
||||
import io.novafoundation.nova.common.validation.Validation
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
|
||||
import io.novafoundation.nova.common.validation.valid
|
||||
import io.novafoundation.nova.common.validation.validationError
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chainsById
|
||||
|
||||
class NetworkAlreadyAddedValidation<P, F>(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val chainIdRequester: suspend (P) -> String,
|
||||
private val ignoreChainModifying: (P) -> Boolean,
|
||||
private val defaultNetworkFailure: (P, Chain) -> F,
|
||||
private val customNetworkWarning: (P, Chain) -> F
|
||||
) : Validation<P, F> {
|
||||
|
||||
override suspend fun validate(value: P): ValidationStatus<F> {
|
||||
val chainId = chainIdRequester(value)
|
||||
|
||||
val chain = chainRegistry.chainsById()[chainId]
|
||||
if (chain != null && !ignoreChainModifying(value)) {
|
||||
return when (chain.source) {
|
||||
Chain.Source.DEFAULT -> validationError(defaultNetworkFailure(value, chain))
|
||||
Chain.Source.CUSTOM -> validationError(customNetworkWarning(value, chain))
|
||||
}
|
||||
}
|
||||
|
||||
return valid()
|
||||
}
|
||||
}
|
||||
|
||||
fun <P, F> ValidationSystemBuilder<P, F>.validateNetworkNotAdded(
|
||||
chainRegistry: ChainRegistry,
|
||||
chainIdRequester: suspend (P) -> String,
|
||||
ignoreChainModifying: (P) -> Boolean,
|
||||
defaultNetworkFailure: (P, Chain) -> F,
|
||||
customNetworkFailure: (P, Chain) -> F
|
||||
) = validate(
|
||||
NetworkAlreadyAddedValidation(chainRegistry, chainIdRequester, ignoreChainModifying, defaultNetworkFailure, customNetworkFailure)
|
||||
)
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.validation.customNode
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
typealias NetworkNodeValidationSystem = ValidationSystem<NetworkNodePayload, NetworkNodeFailure>
|
||||
typealias NetworkNodeValidationSystemBuilder = ValidationSystemBuilder<NetworkNodePayload, NetworkNodeFailure>
|
||||
|
||||
class NetworkNodePayload(
|
||||
val chain: Chain,
|
||||
val nodeUrl: String
|
||||
)
|
||||
|
||||
sealed interface NetworkNodeFailure {
|
||||
|
||||
class NodeAlreadyExists(val node: Chain.Node) : NetworkNodeFailure
|
||||
|
||||
class WrongNetwork(val chain: Chain) : NetworkNodeFailure
|
||||
|
||||
object NodeIsNotAlive : NetworkNodeFailure
|
||||
}
|
||||
|
||||
fun NetworkNodeValidationSystemBuilder.validateNetworkNodeIsAlive(
|
||||
nodeHealthStateCheckRequest: suspend (NetworkNodePayload) -> Unit
|
||||
) = validateNetworkNodeIsAlive(
|
||||
nodeHealthStateCheckRequest,
|
||||
nodeUrl = { it.nodeUrl },
|
||||
failure = { NetworkNodeFailure.NodeIsNotAlive }
|
||||
)
|
||||
|
||||
fun NetworkNodeValidationSystemBuilder.validateNodeSupportedByNetwork(
|
||||
nodeChainIdRequester: suspend (NetworkNodePayload) -> String
|
||||
) = nodeSupportedByNetworkValidation(
|
||||
nodeChainIdRequester = { nodeChainIdRequester(it) },
|
||||
originalChainId = { it.chain.id },
|
||||
failure = { NetworkNodeFailure.WrongNetwork(it.chain) }
|
||||
)
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.validation.customNode
|
||||
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.common.validation.Validation
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
|
||||
import io.novafoundation.nova.common.validation.valid
|
||||
import io.novafoundation.nova.common.validation.validationError
|
||||
|
||||
class NodeAlreadyAddedValidation : Validation<NetworkNodePayload, NetworkNodeFailure> {
|
||||
|
||||
override suspend fun validate(value: NetworkNodePayload): ValidationStatus<NetworkNodeFailure> {
|
||||
try {
|
||||
val node = value.chain.nodes
|
||||
.nodes
|
||||
.firstOrNull { it.unformattedUrl == value.nodeUrl.normalize() }
|
||||
|
||||
if (node != null) {
|
||||
return validationError(NetworkNodeFailure.NodeAlreadyExists(node))
|
||||
}
|
||||
|
||||
return valid()
|
||||
} catch (e: Exception) {
|
||||
return validationError(NetworkNodeFailure.NodeIsNotAlive)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.normalize(): String {
|
||||
return Urls.normalizePath(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun ValidationSystemBuilder<NetworkNodePayload, NetworkNodeFailure>.validateNodeNotAdded() = validate(
|
||||
NodeAlreadyAddedValidation()
|
||||
)
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.validation.customNode
|
||||
|
||||
import io.novafoundation.nova.common.validation.Validation
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
|
||||
import io.novafoundation.nova.common.validation.valid
|
||||
import io.novafoundation.nova.common.validation.validationError
|
||||
import java.lang.Exception
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class NetworkNodeIsAliveValidation<P, F>(
|
||||
private val nodeHealthStateCheckRequest: suspend (P) -> Unit,
|
||||
private val nodeUrl: (P) -> String,
|
||||
private val failure: (P) -> F
|
||||
) : Validation<P, F> {
|
||||
|
||||
override suspend fun validate(value: P): ValidationStatus<F> {
|
||||
return try {
|
||||
val url = nodeUrl(value)
|
||||
require(url.startsWith("wss://") || url.startsWith("ws://"))
|
||||
|
||||
withTimeout(10.seconds) { nodeHealthStateCheckRequest(value) }
|
||||
|
||||
valid()
|
||||
} catch (e: Exception) {
|
||||
validationError(failure(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <P, F> ValidationSystemBuilder<P, F>.validateNetworkNodeIsAlive(
|
||||
nodeHealthStateCheckRequest: suspend (P) -> Unit,
|
||||
nodeUrl: (P) -> String,
|
||||
failure: (P) -> F
|
||||
) = validate(
|
||||
NetworkNodeIsAliveValidation(nodeHealthStateCheckRequest, nodeUrl, failure)
|
||||
)
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.domain.validation.customNode
|
||||
|
||||
import io.novafoundation.nova.common.validation.Validation
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.common.validation.ValidationSystemBuilder
|
||||
import io.novafoundation.nova.common.validation.validOrError
|
||||
|
||||
class NodeSupportedByNetworkValidation<P, F>(
|
||||
private val nodeChainIdRequester: suspend (P) -> String,
|
||||
private val originalChainId: (P) -> String?,
|
||||
private val failure: (P) -> F
|
||||
) : Validation<P, F> {
|
||||
|
||||
override suspend fun validate(value: P): ValidationStatus<F> {
|
||||
val nodeChainId = nodeChainIdRequester(value)
|
||||
|
||||
return validOrError(nodeChainId == originalChainId(value)) {
|
||||
failure(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <P, F> ValidationSystemBuilder<P, F>.nodeSupportedByNetworkValidation(
|
||||
nodeChainIdRequester: suspend (P) -> String,
|
||||
originalChainId: (P) -> String?,
|
||||
failure: (P) -> F
|
||||
) = validate(
|
||||
NodeSupportedByNetworkValidation(
|
||||
nodeChainIdRequester,
|
||||
originalChainId,
|
||||
failure
|
||||
)
|
||||
)
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.assetIcons
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.FragmentAppearanceBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent
|
||||
|
||||
class AppearanceFragment : BaseFragment<AppearanceViewModel, FragmentAppearanceBinding>() {
|
||||
|
||||
override fun createBinding() = FragmentAppearanceBinding.inflate(layoutInflater)
|
||||
|
||||
override fun initViews() {
|
||||
binder.appearanceToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
|
||||
binder.appearanceWhiteButton.setOnClickListener { viewModel.selectWhiteIcon() }
|
||||
binder.appearanceColoredButton.setOnClickListener { viewModel.selectColoredIcon() }
|
||||
|
||||
binder.appearanceWhiteButton.background = getRippleDrawable(cornerSizeInDp = 10)
|
||||
binder.appearanceColoredButton.background = getRippleDrawable(cornerSizeInDp = 10)
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<SettingsFeatureComponent>(
|
||||
requireContext(),
|
||||
SettingsFeatureApi::class.java
|
||||
)
|
||||
.appearanceFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: AppearanceViewModel) {
|
||||
viewModel.assetIconsStateFlow.observe {
|
||||
binder.appearanceWhiteIcon.isSelected = it.whiteActive
|
||||
binder.appearanceWhiteText.isSelected = it.whiteActive
|
||||
|
||||
binder.appearanceColoredIcon.isSelected = it.coloredActive
|
||||
binder.appearanceColoredText.isSelected = it.coloredActive
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.assetIcons
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.data.model.AssetIconMode
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.AppearanceInteractor
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class AssetIconsStateModel(
|
||||
val whiteActive: Boolean,
|
||||
val coloredActive: Boolean
|
||||
)
|
||||
|
||||
class AppearanceViewModel(
|
||||
private val interactor: AppearanceInteractor,
|
||||
private val router: SettingsRouter
|
||||
) : BaseViewModel() {
|
||||
|
||||
val assetIconsStateFlow = interactor.assetIconModeFlow()
|
||||
.map {
|
||||
AssetIconsStateModel(
|
||||
whiteActive = it == AssetIconMode.WHITE,
|
||||
coloredActive = it == AssetIconMode.COLORED
|
||||
)
|
||||
}
|
||||
|
||||
fun selectWhiteIcon() {
|
||||
interactor.setIconMode(AssetIconMode.WHITE)
|
||||
router.returnToWallet()
|
||||
}
|
||||
|
||||
fun selectColoredIcon() {
|
||||
interactor.setIconMode(AssetIconMode.COLORED)
|
||||
router.returnToWallet()
|
||||
}
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.assetIcons.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.assetIcons.AppearanceFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
AppearanceModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface AppearanceComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment
|
||||
): AppearanceComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: AppearanceFragment)
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.assetIcons.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.AppearanceInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.assetIcons.AppearanceViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class AppearanceModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(AppearanceViewModel::class)
|
||||
fun provideViewModel(
|
||||
interactor: AppearanceInteractor,
|
||||
router: SettingsRouter
|
||||
): ViewModel {
|
||||
return AppearanceViewModel(
|
||||
interactor,
|
||||
router
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): AppearanceViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(AppearanceViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.backupDiff
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import io.novafoundation.nova.common.utils.addColor
|
||||
import io.novafoundation.nova.common.utils.formatting.spannable.SpannableFormatter
|
||||
import io.novafoundation.nova.common.view.bottomSheet.BaseBottomSheet
|
||||
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_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.FragmentBackupDiffBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.backupDiff.adapter.CloudBackupDiffAdapter
|
||||
|
||||
class CloudBackupDiffBottomSheet(
|
||||
context: Context,
|
||||
private val payload: Payload,
|
||||
onApply: (CloudBackupDiff, CloudBackup) -> Unit,
|
||||
) : BaseBottomSheet<FragmentBackupDiffBinding>(context) {
|
||||
|
||||
override val binder: FragmentBackupDiffBinding = FragmentBackupDiffBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
class Payload(val diffList: List<Any>, val cloudBackupDiff: CloudBackupDiff, val cloudBackup: CloudBackup)
|
||||
|
||||
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
CloudBackupDiffAdapter()
|
||||
}
|
||||
|
||||
init {
|
||||
binder.backupDiffSubtitle.text = buildSubtitleText()
|
||||
binder.backupDiffList.adapter = adapter
|
||||
|
||||
binder.backupDiffCancel.setOnClickListener { dismiss() }
|
||||
binder.backupDiffApply.setOnClickListener {
|
||||
onApply(payload.cloudBackupDiff, payload.cloudBackup)
|
||||
dismiss()
|
||||
}
|
||||
adapter.submitList(payload.diffList)
|
||||
}
|
||||
|
||||
private fun buildSubtitleText(): CharSequence {
|
||||
return SpannableFormatter.format(
|
||||
context.getString(R.string.backup_diff_subtitle),
|
||||
context.getString(R.string.backup_diff_subtitle_highlighted).addColor(context.getColor(R.color.text_primary))
|
||||
)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.backupDiff.adapter
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import io.novafoundation.nova.common.view.ChipLabelModel
|
||||
|
||||
interface CloudBackupDiffRVItem
|
||||
|
||||
class CloudBackupDiffGroupRVItem(
|
||||
val chipModel: ChipLabelModel
|
||||
) : CloudBackupDiffRVItem
|
||||
|
||||
class AccountDiffRVItem(
|
||||
val id: String,
|
||||
val icon: Drawable,
|
||||
val title: String,
|
||||
val state: String,
|
||||
val stateColorRes: Int,
|
||||
val stateIconRes: Int?,
|
||||
) : CloudBackupDiffRVItem
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.backupDiff.adapter
|
||||
|
||||
import android.view.ViewGroup
|
||||
import io.novafoundation.nova.common.list.BaseGroupedDiffCallback
|
||||
import io.novafoundation.nova.common.list.GroupedListAdapter
|
||||
import io.novafoundation.nova.common.list.GroupedListHolder
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setDrawableStart
|
||||
import io.novafoundation.nova.common.view.ChipLabelView
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.listing.holders.AccountChipHolder
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.ItemCloudBackupAccountDiffBinding
|
||||
|
||||
class CloudBackupDiffAdapter : GroupedListAdapter<CloudBackupDiffGroupRVItem, AccountDiffRVItem>(BackupAccountDiffCallback()) {
|
||||
|
||||
override fun createGroupViewHolder(parent: ViewGroup): GroupedListHolder {
|
||||
return AccountChipHolder(ChipLabelView(parent.context))
|
||||
}
|
||||
|
||||
override fun createChildViewHolder(parent: ViewGroup): GroupedListHolder {
|
||||
return AccountDiffHolder(ItemCloudBackupAccountDiffBinding.inflate(parent.inflater(), parent, false))
|
||||
}
|
||||
|
||||
override fun bindGroup(holder: GroupedListHolder, group: CloudBackupDiffGroupRVItem) {
|
||||
(holder as AccountChipHolder).bind(group.chipModel)
|
||||
}
|
||||
|
||||
override fun bindChild(holder: GroupedListHolder, child: AccountDiffRVItem) {
|
||||
(holder as AccountDiffHolder).bind(child)
|
||||
}
|
||||
}
|
||||
|
||||
class AccountDiffHolder(private val binder: ItemCloudBackupAccountDiffBinding) : GroupedListHolder(binder.root) {
|
||||
|
||||
fun bind(accountModel: AccountDiffRVItem) = with(binder) {
|
||||
itemCloudBackupAccountDiffIcon.setImageDrawable(accountModel.icon)
|
||||
itemCloudBackupAccountDiffName.text = accountModel.title
|
||||
itemCloudBackupAccountDiffState.text = accountModel.state
|
||||
itemCloudBackupAccountDiffState.setTextColor(root.context.getColor(accountModel.stateColorRes))
|
||||
itemCloudBackupAccountDiffState.setDrawableStart(accountModel.stateIconRes, paddingInDp = 4)
|
||||
}
|
||||
}
|
||||
|
||||
private class BackupAccountDiffCallback : BaseGroupedDiffCallback<CloudBackupDiffGroupRVItem, AccountDiffRVItem>(CloudBackupDiffGroupRVItem::class.java) {
|
||||
|
||||
override fun areGroupItemsTheSame(oldItem: CloudBackupDiffGroupRVItem, newItem: CloudBackupDiffGroupRVItem): Boolean {
|
||||
return oldItem.chipModel.title == newItem.chipModel.title
|
||||
}
|
||||
|
||||
override fun areGroupContentsTheSame(oldItem: CloudBackupDiffGroupRVItem, newItem: CloudBackupDiffGroupRVItem): Boolean {
|
||||
return oldItem.chipModel.title == newItem.chipModel.title
|
||||
}
|
||||
|
||||
override fun areChildItemsTheSame(oldItem: AccountDiffRVItem, newItem: AccountDiffRVItem): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areChildContentsTheSame(oldItem: AccountDiffRVItem, newItem: AccountDiffRVItem): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.setupConfirmationDialog
|
||||
import io.novafoundation.nova.common.mixin.impl.setupCustomDialogDisplayer
|
||||
import io.novafoundation.nova.common.utils.progress.observeProgressDialog
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.observeActionBottomSheet
|
||||
import io.novafoundation.nova.common.view.input.selector.setupListSelectorMixin
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.FragmentBackupSettingsBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.backupDiff.CloudBackupDiffBottomSheet
|
||||
|
||||
class BackupSettingsFragment : BaseFragment<BackupSettingsViewModel, FragmentBackupSettingsBinding>() {
|
||||
|
||||
override fun createBinding() = FragmentBackupSettingsBinding.inflate(layoutInflater)
|
||||
|
||||
override fun initViews() {
|
||||
binder.backupSettingsToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
binder.backupStateView.setOnClickListener { viewModel.cloudBackupManageClicked() }
|
||||
binder.backupStateView.setProblemClickListener() { viewModel.problemButtonClicked() }
|
||||
binder.backupSettingsSwitcher.setOnClickListener { viewModel.backupSwitcherClicked() }
|
||||
binder.backupSettingsManualBtn.setOnClickListener { viewModel.manualBackupClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<SettingsFeatureComponent>(
|
||||
requireContext(),
|
||||
SettingsFeatureApi::class.java
|
||||
)
|
||||
.backupSettings()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: BackupSettingsViewModel) {
|
||||
setupCustomDialogDisplayer(viewModel)
|
||||
observeProgressDialog(viewModel.progressDialogMixin)
|
||||
observeActionBottomSheet(viewModel)
|
||||
setupListSelectorMixin(viewModel.listSelectorMixin)
|
||||
setupConfirmationDialog(R.style.AccentNegativeAlertDialogTheme_Reversed, viewModel.negativeConfirmationAwaitableAction)
|
||||
setupConfirmationDialog(R.style.AccentAlertDialogTheme, viewModel.neutralConfirmationAwaitableAction)
|
||||
|
||||
viewModel.cloudBackupEnabled.observe { enabled ->
|
||||
binder.backupSettingsSwitcher.setChecked(enabled)
|
||||
}
|
||||
|
||||
viewModel.cloudBackupStateModel.observe { state ->
|
||||
binder.backupStateView.setState(state)
|
||||
}
|
||||
|
||||
viewModel.cloudBackupChangesLiveData.observeEvent {
|
||||
showBackupDiffBottomSheet(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBackupDiffBottomSheet(payload: CloudBackupDiffBottomSheet.Payload) {
|
||||
val bottomSheet = CloudBackupDiffBottomSheet(
|
||||
requireContext(),
|
||||
payload,
|
||||
onApply = { diff, cloudBackup -> viewModel.applyBackupDestructiveChanges(diff, cloudBackup) }
|
||||
)
|
||||
|
||||
bottomSheet.show()
|
||||
}
|
||||
}
|
||||
+431
@@ -0,0 +1,431 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.base.showError
|
||||
import io.novafoundation.nova.common.list.toListWithHeaders
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ConfirmationDialogInfo
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
|
||||
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer
|
||||
import io.novafoundation.nova.common.mixin.api.displayDialogOrNothing
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.event
|
||||
import io.novafoundation.nova.common.utils.flatMap
|
||||
import io.novafoundation.nova.common.utils.progress.ProgressDialogMixinFactory
|
||||
import io.novafoundation.nova.common.utils.progress.startProgress
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncher
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncherFactory
|
||||
import io.novafoundation.nova.common.view.input.selector.ListSelectorMixin
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.MetaAccountTypePresentationMapper
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.ChangeBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.ChangeBackupPasswordRequester
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.RestoreBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.RestoreBackupPasswordRequester
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.createPassword.SyncWalletsBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.createPassword.SyncWalletsBackupPasswordRequester
|
||||
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.CloudBackupNotFound
|
||||
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.presenter.action.launchCloudBackupChangesAction
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.action.launchCorruptedBackupFoundAction
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.action.launchDeleteBackupAction
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.action.launchDeprecatedPasswordAction
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.confirmation.awaitBackupDestructiveChangesConfirmation
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.confirmation.awaitDeleteBackupConfirmation
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.handlers.showCloudBackupUnknownError
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.mapCloudBackupSyncFailed
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.mapDeleteBackupFailureToUi
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.presenter.errorHandling.mapWriteBackupFailureToUi
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.CloudBackupSettingsInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.model.CloudBackupChangedAccount
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.backupDiff.CloudBackupDiffBottomSheet
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.backupDiff.adapter.AccountDiffRVItem
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.backupDiff.adapter.CloudBackupDiffGroupRVItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class BackupSettingsViewModel(
|
||||
private val resourceManager: ResourceManager,
|
||||
private val router: SettingsRouter,
|
||||
private val accountInteractor: AccountInteractor,
|
||||
private val cloudBackupSettingsInteractor: CloudBackupSettingsInteractor,
|
||||
private val syncWalletsBackupPasswordCommunicator: SyncWalletsBackupPasswordCommunicator,
|
||||
private val changeBackupPasswordCommunicator: ChangeBackupPasswordCommunicator,
|
||||
private val restoreBackupPasswordCommunicator: RestoreBackupPasswordCommunicator,
|
||||
private val actionBottomSheetLauncherFactory: ActionBottomSheetLauncherFactory,
|
||||
private val accountTypePresentationMapper: MetaAccountTypePresentationMapper,
|
||||
private val walletUiUseCase: WalletUiUseCase,
|
||||
private val progressDialogMixinFactory: ProgressDialogMixinFactory,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
listSelectorMixinFactory: ListSelectorMixin.Factory,
|
||||
customDialogProvider: CustomDialogDisplayer.Presentation
|
||||
) : BaseViewModel(),
|
||||
ActionBottomSheetLauncher by actionBottomSheetLauncherFactory.create(),
|
||||
CustomDialogDisplayer.Presentation by customDialogProvider {
|
||||
|
||||
val progressDialogMixin = progressDialogMixinFactory.create()
|
||||
|
||||
val negativeConfirmationAwaitableAction = actionAwaitableMixinFactory.confirmingAction<ConfirmationDialogInfo>()
|
||||
|
||||
val neutralConfirmationAwaitableAction = actionAwaitableMixinFactory.confirmingAction<ConfirmationDialogInfo>()
|
||||
|
||||
val listSelectorMixin = listSelectorMixinFactory.create(viewModelScope)
|
||||
|
||||
private val _isSyncing = MutableStateFlow(false)
|
||||
val isSyncing: Flow<Boolean> = _isSyncing
|
||||
|
||||
private val syncedState = MutableStateFlow<BackupSyncOutcome>(BackupSyncOutcome.Ok)
|
||||
|
||||
private val lastSync = cloudBackupSettingsInteractor.observeLastSyncedTime()
|
||||
|
||||
val cloudBackupEnabled = MutableStateFlow(false)
|
||||
|
||||
val cloudBackupStateModel: Flow<CloudBackupStateModel> = combine(
|
||||
cloudBackupEnabled,
|
||||
_isSyncing,
|
||||
syncedState,
|
||||
lastSync
|
||||
) { backupEnabled, syncingInProgress, state, lastSync ->
|
||||
mapCloudBackupStateModel(resourceManager, backupEnabled, syncingInProgress, state, lastSync)
|
||||
}
|
||||
|
||||
private val _cloudBackupChangesLiveData = MutableLiveData<Event<CloudBackupDiffBottomSheet.Payload>>()
|
||||
val cloudBackupChangesLiveData = _cloudBackupChangesLiveData
|
||||
|
||||
init {
|
||||
syncCloudBackupState()
|
||||
observeRequesterResults()
|
||||
|
||||
launch {
|
||||
cloudBackupEnabled.value = cloudBackupSettingsInteractor.isSyncCloudBackupEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun backupSwitcherClicked() {
|
||||
launch {
|
||||
if (cloudBackupSettingsInteractor.isSyncCloudBackupEnabled()) {
|
||||
cloudBackupEnabled.value = false
|
||||
cloudBackupSettingsInteractor.setCloudBackupSyncEnabled(false)
|
||||
} else {
|
||||
cloudBackupEnabled.value = true
|
||||
|
||||
syncCloudBackupOnSwitcher()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun manualBackupClicked() {
|
||||
launch {
|
||||
if (accountInteractor.hasSecretsAccounts()) {
|
||||
router.openManualBackup()
|
||||
} else {
|
||||
showError(
|
||||
resourceManager.getString(R.string.backup_settings_no_wallets_error_title),
|
||||
resourceManager.getString(R.string.backup_settings_no_wallets_error_message)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cloudBackupManageClicked() {
|
||||
if (_isSyncing.value) return
|
||||
|
||||
when (syncedState.value) {
|
||||
BackupSyncOutcome.StorageAuthFailed -> return
|
||||
|
||||
BackupSyncOutcome.EmptyPassword,
|
||||
BackupSyncOutcome.UnknownPassword,
|
||||
BackupSyncOutcome.CorruptedBackup -> {
|
||||
listSelectorMixin.showSelector(
|
||||
R.string.manage_cloud_backup,
|
||||
listOf(manageBackupDeleteBackupItem())
|
||||
)
|
||||
}
|
||||
|
||||
BackupSyncOutcome.Ok,
|
||||
is BackupSyncOutcome.DestructiveDiff,
|
||||
BackupSyncOutcome.UnknownError -> {
|
||||
listSelectorMixin.showSelector(
|
||||
R.string.manage_cloud_backup,
|
||||
listOf(manageBackupChangePasswordItem(), manageBackupDeleteBackupItem())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun problemButtonClicked() {
|
||||
when (val value = syncedState.value) {
|
||||
BackupSyncOutcome.EmptyPassword,
|
||||
BackupSyncOutcome.UnknownPassword -> openRestorePassword()
|
||||
|
||||
BackupSyncOutcome.CorruptedBackup -> showCorruptedBackupActionDialog()
|
||||
is BackupSyncOutcome.DestructiveDiff -> openCloudBackupDiffScreen(value.cloudBackupDiff, value.cloudBackup)
|
||||
BackupSyncOutcome.StorageAuthFailed -> initSignInToCloud()
|
||||
|
||||
BackupSyncOutcome.UnknownError -> showCloudBackupUnknownError(resourceManager)
|
||||
|
||||
BackupSyncOutcome.Ok -> {}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyBackupDestructiveChanges(cloudBackupDiff: CloudBackupDiff, cloudBackup: CloudBackup) {
|
||||
launch {
|
||||
neutralConfirmationAwaitableAction.awaitBackupDestructiveChangesConfirmation(resourceManager)
|
||||
|
||||
_isSyncing.value = true
|
||||
|
||||
cloudBackupSettingsInteractor.applyBackupAccountDiff(cloudBackupDiff, cloudBackup)
|
||||
.onSuccess { syncedState.value = BackupSyncOutcome.Ok }
|
||||
.onFailure { showError(mapWriteBackupFailureToUi(resourceManager, it)) }
|
||||
|
||||
_isSyncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun openDestructiveDiffAction(diff: CloudBackupDiff, cloudBackup: CloudBackup) {
|
||||
launchCloudBackupChangesAction(resourceManager) {
|
||||
openCloudBackupDiffScreen(diff, cloudBackup)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openCloudBackupDiffScreen(diff: CloudBackupDiff, cloudBackup: CloudBackup) {
|
||||
launch {
|
||||
val sortedDiff = cloudBackupSettingsInteractor.prepareSortedLocalChangesFromDiff(diff)
|
||||
val cloudBackupChangesList = sortedDiff.toListWithHeaders(
|
||||
keyMapper = { type, _ -> accountTypePresentationMapper.mapTypeToChipLabel(type)?.let { CloudBackupDiffGroupRVItem(it) } },
|
||||
valueMapper = { mapMetaAccountDiffToUi(it) }
|
||||
)
|
||||
|
||||
_cloudBackupChangesLiveData.value = CloudBackupDiffBottomSheet.Payload(cloudBackupChangesList, diff, cloudBackup).event()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Throwable.toEnableBackupSyncState(): BackupSyncOutcome {
|
||||
return when (this) {
|
||||
is PasswordNotSaved -> BackupSyncOutcome.EmptyPassword
|
||||
is InvalidBackupPasswordError -> BackupSyncOutcome.UnknownPassword
|
||||
is CannotApplyNonDestructiveDiff -> BackupSyncOutcome.DestructiveDiff(cloudBackupDiff, cloudBackup)
|
||||
// not found backup is ok when we enable backup and when we start initial sync since we will create a new backup
|
||||
is FetchBackupError.BackupNotFound -> BackupSyncOutcome.Ok
|
||||
is FetchBackupError.CorruptedBackup -> BackupSyncOutcome.CorruptedBackup
|
||||
is FetchBackupError.Other -> BackupSyncOutcome.UnknownError
|
||||
is FetchBackupError.AuthFailed -> BackupSyncOutcome.StorageAuthFailed
|
||||
else -> BackupSyncOutcome.UnknownError
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncCloudBackupState() = launch {
|
||||
if (cloudBackupSettingsInteractor.isSyncCloudBackupEnabled()) {
|
||||
runSyncWithProgress { result ->
|
||||
result.onFailure { throwable ->
|
||||
if (throwable is CloudBackupNotFound) {
|
||||
writeBackupToCloudAndSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun writeBackupToCloudAndSync() {
|
||||
cloudBackupSettingsInteractor.writeLocalBackupToCloud()
|
||||
.flatMap {
|
||||
cloudBackupSettingsInteractor.syncCloudBackup()
|
||||
}.handleSyncBackupResult()
|
||||
}
|
||||
|
||||
private suspend fun syncCloudBackupOnSwitcher() = runSyncWithProgress { result ->
|
||||
result.onSuccess { cloudBackupSettingsInteractor.setCloudBackupSyncEnabled(true) }
|
||||
.onFailure { throwable ->
|
||||
|
||||
when (throwable) {
|
||||
is CloudBackupNotFound -> {
|
||||
syncWalletsBackupPasswordCommunicator.openRequest(SyncWalletsBackupPasswordRequester.EmptyRequest)
|
||||
}
|
||||
|
||||
else -> {
|
||||
cloudBackupSettingsInteractor.setCloudBackupSyncEnabled(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun runSyncWithProgress(action: (Result<Unit>) -> Unit) {
|
||||
_isSyncing.value = true
|
||||
|
||||
val result = cloudBackupSettingsInteractor.syncCloudBackup()
|
||||
.handleSyncBackupResult()
|
||||
|
||||
action(result)
|
||||
|
||||
_isSyncing.value = false
|
||||
}
|
||||
|
||||
private fun Result<Unit>.handleSyncBackupResult(): Result<Unit> {
|
||||
return onSuccess { syncedState.value = BackupSyncOutcome.Ok; }
|
||||
.onFailure { throwable ->
|
||||
val state = throwable.toEnableBackupSyncState()
|
||||
syncedState.value = state
|
||||
if (state == BackupSyncOutcome.EmptyPassword) {
|
||||
openRestorePassword()
|
||||
} else {
|
||||
handleBackupError(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBackupError(throwable: Throwable) {
|
||||
val payload = mapCloudBackupSyncFailed(
|
||||
resourceManager,
|
||||
throwable,
|
||||
onDestructiveBackupFound = ::openDestructiveDiffAction,
|
||||
onPasswordDeprecated = ::showPasswordDeprecatedActionDialog,
|
||||
onCorruptedBackup = ::showCorruptedBackupActionDialog,
|
||||
initSignIn = ::initSignInToCloud
|
||||
)
|
||||
|
||||
displayDialogOrNothing(payload)
|
||||
}
|
||||
|
||||
private fun manageBackupChangePasswordItem(): ListSelectorMixin.Item {
|
||||
return ListSelectorMixin.Item(
|
||||
R.drawable.ic_pin,
|
||||
R.color.icon_primary,
|
||||
R.string.common_change_password,
|
||||
R.color.text_primary,
|
||||
::onChangePasswordClicked
|
||||
)
|
||||
}
|
||||
|
||||
private fun manageBackupDeleteBackupItem(): ListSelectorMixin.Item {
|
||||
return ListSelectorMixin.Item(
|
||||
R.drawable.ic_delete,
|
||||
R.color.icon_negative,
|
||||
R.string.backup_settings_delete_backup,
|
||||
R.color.text_negative,
|
||||
::onDeleteBackupClicked
|
||||
)
|
||||
}
|
||||
|
||||
private fun onChangePasswordClicked() {
|
||||
changeBackupPasswordCommunicator.openRequest(ChangeBackupPasswordRequester.EmptyRequest)
|
||||
}
|
||||
|
||||
private fun onDeleteBackupClicked() {
|
||||
launchDeleteBackupAction(resourceManager, ::confirmCloudBackupDelete)
|
||||
}
|
||||
|
||||
private fun observeRequesterResults() {
|
||||
changeBackupPasswordCommunicator.responseFlow.syncBackupOnEach()
|
||||
restoreBackupPasswordCommunicator.responseFlow.syncBackupOnEach()
|
||||
syncWalletsBackupPasswordCommunicator.responseFlow.syncBackupOnEach()
|
||||
|
||||
syncWalletsBackupPasswordCommunicator.responseFlow.onEach { response ->
|
||||
cloudBackupEnabled.value = cloudBackupSettingsInteractor.isSyncCloudBackupEnabled()
|
||||
syncedState.value = BackupSyncOutcome.Ok
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
private fun Flow<Any>.syncBackupOnEach() {
|
||||
this.onEach {
|
||||
syncCloudBackupState()
|
||||
}
|
||||
.launchIn(this@BackupSettingsViewModel)
|
||||
}
|
||||
|
||||
private fun showPasswordDeprecatedActionDialog() {
|
||||
launchDeprecatedPasswordAction(resourceManager, ::openRestorePassword)
|
||||
}
|
||||
|
||||
private fun showCorruptedBackupActionDialog() {
|
||||
launchCorruptedBackupFoundAction(resourceManager, ::confirmCloudBackupDelete)
|
||||
}
|
||||
|
||||
private fun openRestorePassword() {
|
||||
restoreBackupPasswordCommunicator.openRequest(RestoreBackupPasswordRequester.EmptyRequest)
|
||||
}
|
||||
|
||||
private fun initSignInToCloud() {
|
||||
launch {
|
||||
cloudBackupSettingsInteractor.signInToCloud()
|
||||
.onSuccess { syncCloudBackupState() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmCloudBackupDelete() {
|
||||
launch {
|
||||
negativeConfirmationAwaitableAction.awaitDeleteBackupConfirmation(resourceManager)
|
||||
|
||||
progressDialogMixin.startProgress(R.string.deleting_backup_progress) {
|
||||
runDeleteBackup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runDeleteBackup() {
|
||||
cloudBackupSettingsInteractor.deleteCloudBackup()
|
||||
.onSuccess {
|
||||
cloudBackupSettingsInteractor.setCloudBackupSyncEnabled(false)
|
||||
cloudBackupEnabled.value = false
|
||||
}
|
||||
.onFailure { throwable ->
|
||||
val titleAndMessage = mapDeleteBackupFailureToUi(resourceManager, throwable)
|
||||
titleAndMessage?.let { showError(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun mapMetaAccountDiffToUi(changedAccount: CloudBackupChangedAccount): AccountDiffRVItem {
|
||||
return with(changedAccount) {
|
||||
val (stateText, stateColorRes, stateIconRes) = mapChangingTypeToUi(changingType)
|
||||
val walletIcon = walletUiUseCase.walletIcon(
|
||||
account.substrateAccountId,
|
||||
account.ethereumAddress,
|
||||
account.chainAccounts.map(CloudBackup.WalletPublicInfo.ChainAccountInfo::accountId)
|
||||
)
|
||||
|
||||
AccountDiffRVItem(
|
||||
id = account.walletId,
|
||||
icon = walletIcon,
|
||||
title = account.name,
|
||||
state = stateText,
|
||||
stateColorRes = stateColorRes,
|
||||
stateIconRes = stateIconRes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapChangingTypeToUi(type: CloudBackupChangedAccount.ChangingType): Triple<String, Int, Int?> {
|
||||
return when (type) {
|
||||
CloudBackupChangedAccount.ChangingType.ADDED -> Triple(resourceManager.getString(R.string.state_new), R.color.text_secondary, null)
|
||||
CloudBackupChangedAccount.ChangingType.REMOVED -> Triple(
|
||||
resourceManager.getString(R.string.state_removed),
|
||||
R.color.text_negative,
|
||||
R.drawable.ic_red_cross
|
||||
)
|
||||
|
||||
CloudBackupChangedAccount.ChangingType.CHANGED -> Triple(
|
||||
resourceManager.getString(R.string.state_changed),
|
||||
R.color.text_warning,
|
||||
R.drawable.ic_warning_filled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings
|
||||
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.CloudBackup
|
||||
import io.novafoundation.nova.feature_cloud_backup_api.domain.model.diff.CloudBackupDiff
|
||||
|
||||
sealed class BackupSyncOutcome {
|
||||
|
||||
object Ok : BackupSyncOutcome()
|
||||
|
||||
object EmptyPassword : BackupSyncOutcome()
|
||||
|
||||
object UnknownPassword : BackupSyncOutcome()
|
||||
|
||||
class DestructiveDiff(val cloudBackupDiff: CloudBackupDiff, val cloudBackup: CloudBackup) : BackupSyncOutcome()
|
||||
|
||||
object StorageAuthFailed : BackupSyncOutcome()
|
||||
|
||||
object CorruptedBackup : BackupSyncOutcome()
|
||||
|
||||
object UnknownError : BackupSyncOutcome()
|
||||
}
|
||||
|
||||
fun BackupSyncOutcome.isError(): Boolean {
|
||||
return this != BackupSyncOutcome.Ok
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings
|
||||
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.formatting.formatDateSinceEpoch
|
||||
import io.novafoundation.nova.common.utils.formatting.formatTime
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import java.util.Date
|
||||
|
||||
fun mapCloudBackupStateModel(
|
||||
resourceManager: ResourceManager,
|
||||
backupEnabled: Boolean,
|
||||
syncingInProgress: Boolean,
|
||||
syncOutcome: BackupSyncOutcome,
|
||||
lastSync: Date?
|
||||
): CloudBackupStateModel {
|
||||
val (stateImage, stateImageTint, stateBackgroundColor) = mapStateImage(backupEnabled, syncOutcome)
|
||||
|
||||
return CloudBackupStateModel(
|
||||
stateImg = stateImage,
|
||||
stateImageTint = stateImageTint,
|
||||
stateColorBackgroundRes = stateBackgroundColor,
|
||||
showProgress = syncingInProgress,
|
||||
title = mapCloudBackupStateTitle(resourceManager, backupEnabled, syncingInProgress, syncOutcome),
|
||||
subtitle = mapCloudBackupStateSubtitle(resourceManager, backupEnabled, lastSync),
|
||||
isClickable = mapCloudBackupClickability(backupEnabled, syncingInProgress, syncOutcome),
|
||||
problemButtonText = mapCloudBackupProblemButton(resourceManager, backupEnabled, syncingInProgress, syncOutcome)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapStateImage(backupEnabled: Boolean, syncOutcome: BackupSyncOutcome) = when {
|
||||
!backupEnabled -> Triple(R.drawable.ic_cloud_backup_status_disabled, R.color.icon_secondary, R.color.waiting_status_background)
|
||||
syncOutcome.isError() -> Triple(R.drawable.ic_cloud_backup_status_warning, R.color.icon_warning, R.color.warning_block_background)
|
||||
else -> Triple(R.drawable.ic_cloud_backup_status_active, R.color.icon_positive, R.color.active_status_background)
|
||||
}
|
||||
|
||||
private fun mapCloudBackupStateTitle(
|
||||
resourceManager: ResourceManager,
|
||||
backupEnabled: Boolean,
|
||||
syncingInProgress: Boolean,
|
||||
syncOutcome: BackupSyncOutcome
|
||||
) = when {
|
||||
syncingInProgress -> resourceManager.getString(R.string.cloud_backup_state_syncing_title)
|
||||
!backupEnabled -> resourceManager.getString(R.string.cloud_backup_state_disabled_title)
|
||||
syncOutcome.isError() -> resourceManager.getString(R.string.cloud_backup_state_unsynced_title)
|
||||
else -> resourceManager.getString(R.string.cloud_backup_state_synced_title)
|
||||
}
|
||||
|
||||
private fun mapCloudBackupStateSubtitle(
|
||||
resourceManager: ResourceManager,
|
||||
backupEnabled: Boolean,
|
||||
lastSync: Date?
|
||||
) = when {
|
||||
!backupEnabled -> resourceManager.getString(R.string.cloud_backup_settings_disabled_state_subtitle)
|
||||
lastSync != null -> resourceManager.getString(
|
||||
R.string.cloud_backup_settings_last_sync,
|
||||
lastSync.formatDateSinceEpoch(resourceManager),
|
||||
resourceManager.formatTime(lastSync)
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun mapCloudBackupClickability(
|
||||
backupEnabled: Boolean,
|
||||
syncingInProgress: Boolean,
|
||||
syncOutcome: BackupSyncOutcome
|
||||
): Boolean {
|
||||
if (!backupEnabled) return false
|
||||
if (syncingInProgress) return false
|
||||
|
||||
return when (syncOutcome) {
|
||||
BackupSyncOutcome.Ok,
|
||||
is BackupSyncOutcome.DestructiveDiff,
|
||||
BackupSyncOutcome.EmptyPassword,
|
||||
BackupSyncOutcome.UnknownPassword,
|
||||
BackupSyncOutcome.CorruptedBackup,
|
||||
BackupSyncOutcome.UnknownError -> true
|
||||
|
||||
BackupSyncOutcome.StorageAuthFailed -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapCloudBackupProblemButton(
|
||||
resourceManager: ResourceManager,
|
||||
backupEnabled: Boolean,
|
||||
syncingInProgress: Boolean,
|
||||
syncOutcome: BackupSyncOutcome
|
||||
): String? {
|
||||
if (!backupEnabled) return null
|
||||
if (syncingInProgress) return null
|
||||
|
||||
return when (syncOutcome) {
|
||||
BackupSyncOutcome.Ok -> null
|
||||
BackupSyncOutcome.CorruptedBackup -> resourceManager.getString(R.string.cloud_backup_settings_backup_errors_button)
|
||||
is BackupSyncOutcome.DestructiveDiff -> resourceManager.getString(R.string.cloud_backup_settings_corrupted_backup_button)
|
||||
|
||||
BackupSyncOutcome.EmptyPassword,
|
||||
BackupSyncOutcome.UnknownPassword -> resourceManager.getString(R.string.cloud_backup_settings_deprecated_password_button)
|
||||
|
||||
BackupSyncOutcome.StorageAuthFailed -> resourceManager.getString(R.string.cloud_backup_settings_not_auth_button)
|
||||
BackupSyncOutcome.UnknownError -> resourceManager.getString(R.string.cloud_backup_settings_other_errors_button)
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings
|
||||
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings.views.CloudBackupStateView
|
||||
|
||||
class CloudBackupStateModel(
|
||||
val stateImg: Int?,
|
||||
val stateImageTint: Int,
|
||||
val stateColorBackgroundRes: Int?,
|
||||
val showProgress: Boolean,
|
||||
val title: String,
|
||||
val subtitle: String?,
|
||||
val isClickable: Boolean,
|
||||
val problemButtonText: String?
|
||||
)
|
||||
|
||||
fun CloudBackupStateView.setState(state: CloudBackupStateModel) {
|
||||
setStateImage(state.stateImg, state.stateImageTint, state.stateColorBackgroundRes)
|
||||
setProgressVisibility(state.showProgress)
|
||||
setTitle(state.title)
|
||||
setSubtitle(state.subtitle)
|
||||
isClickable = state.isClickable
|
||||
showMoreButton(state.isClickable)
|
||||
setProblemText(state.problemButtonText)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings.BackupSettingsFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
CloudBackupSettingsModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface CloudBackupSettingsComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment
|
||||
): CloudBackupSettingsComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: BackupSettingsFragment)
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.progress.ProgressDialogMixinFactory
|
||||
import io.novafoundation.nova.common.view.bottomSheet.action.ActionBottomSheetLauncherFactory
|
||||
import io.novafoundation.nova.common.view.input.selector.ListSelectorMixin
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.common.listing.MetaAccountTypePresentationMapper
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.ChangeBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.changePassword.RestoreBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.cloudBackup.createPassword.SyncWalletsBackupPasswordCommunicator
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.CloudBackupSettingsInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings.BackupSettingsViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class CloudBackupSettingsModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(BackupSettingsViewModel::class)
|
||||
fun provideViewModel(
|
||||
resourceManager: ResourceManager,
|
||||
router: SettingsRouter,
|
||||
accountInteractor: AccountInteractor,
|
||||
cloudBackupSettingsInteractor: CloudBackupSettingsInteractor,
|
||||
syncWalletsBackupPasswordCommunicator: SyncWalletsBackupPasswordCommunicator,
|
||||
changeBackupPasswordCommunicator: ChangeBackupPasswordCommunicator,
|
||||
restoreBackupPasswordCommunicator: RestoreBackupPasswordCommunicator,
|
||||
actionBottomSheetLauncherFactory: ActionBottomSheetLauncherFactory,
|
||||
accountTypePresentationMapper: MetaAccountTypePresentationMapper,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
progressDialogMixinFactory: ProgressDialogMixinFactory,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
listSelectorMixinFactory: ListSelectorMixin.Factory,
|
||||
customDialogProvider: CustomDialogDisplayer.Presentation
|
||||
): ViewModel {
|
||||
return BackupSettingsViewModel(
|
||||
resourceManager,
|
||||
router,
|
||||
accountInteractor,
|
||||
cloudBackupSettingsInteractor,
|
||||
syncWalletsBackupPasswordCommunicator,
|
||||
changeBackupPasswordCommunicator,
|
||||
restoreBackupPasswordCommunicator,
|
||||
actionBottomSheetLauncherFactory,
|
||||
accountTypePresentationMapper,
|
||||
walletUiUseCase,
|
||||
progressDialogMixinFactory,
|
||||
actionAwaitableMixinFactory,
|
||||
listSelectorMixinFactory,
|
||||
customDialogProvider
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(
|
||||
fragment: Fragment,
|
||||
viewModelFactory: ViewModelProvider.Factory
|
||||
): BackupSettingsViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(BackupSettingsViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.cloudBackup.settings.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import io.novafoundation.nova.common.utils.getRippleMask
|
||||
import io.novafoundation.nova.common.utils.getRoundedCornerDrawable
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setImageTint
|
||||
import io.novafoundation.nova.common.utils.setTextOrHide
|
||||
import io.novafoundation.nova.common.utils.withRippleMask
|
||||
import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable
|
||||
import io.novafoundation.nova.common.view.shape.ovalDrawable
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.ViewCloudBackupStateBinding
|
||||
|
||||
class CloudBackupStateView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0,
|
||||
) : ConstraintLayout(context, attrs, defStyle) {
|
||||
|
||||
private val binder = ViewCloudBackupStateBinding.inflate(inflater(), this, true)
|
||||
|
||||
private val stateImage: ImageView
|
||||
get() = binder.backupStateImg
|
||||
|
||||
private val progress: ProgressBar
|
||||
get() = binder.backupStateProgress
|
||||
|
||||
private val title: TextView
|
||||
get() = binder.backupStateTitle
|
||||
|
||||
private val subtitle: TextView
|
||||
get() = binder.backupStateSubtitle
|
||||
|
||||
private val more: View
|
||||
get() = binder.backupStateMore
|
||||
|
||||
private val divider: View
|
||||
get() = binder.backupStateDivider
|
||||
|
||||
private val problemButton: TextView
|
||||
get() = binder.backupStateProblemBtn
|
||||
|
||||
init {
|
||||
background = getRoundedCornerDrawable(fillColorRes = R.color.block_background).withRippleMask()
|
||||
problemButton.background = context.getRoundedCornerDrawable(fillColorRes = null).withRippleMask(getRippleMask(cornerSizeDp = 8))
|
||||
}
|
||||
|
||||
fun setStateImage(resId: Int?, stateImageTiniRes: Int?, backgroundColorRes: Int?) {
|
||||
resId?.let { stateImage.setImageResource(it) } ?: stateImage.setImageDrawable(null)
|
||||
stateImage.setImageTint(stateImageTiniRes?.let { context.getColor(it) })
|
||||
stateImage.background = backgroundColorRes?.let { ovalDrawable(context.getColor(it)) }
|
||||
}
|
||||
|
||||
fun setProgressVisibility(visible: Boolean) {
|
||||
progress.isVisible = visible
|
||||
stateImage.isGone = visible
|
||||
}
|
||||
|
||||
fun setTitle(titleText: String) {
|
||||
title.text = titleText
|
||||
}
|
||||
|
||||
fun setSubtitle(text: String?) {
|
||||
subtitle.setTextOrHide(text)
|
||||
}
|
||||
|
||||
fun setProblemText(text: String?) {
|
||||
divider.isVisible = text != null
|
||||
problemButton.isVisible = text != null
|
||||
problemButton.text = text
|
||||
}
|
||||
|
||||
fun setProblemClickListener(listener: OnClickListener?) {
|
||||
problemButton.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
fun showMoreButton(show: Boolean) {
|
||||
more.isVisible = show
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main
|
||||
|
||||
import android.os.Bundle
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.makeGone
|
||||
import io.novafoundation.nova.common.utils.setupWithViewPager2
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.FragmentAddNetworkMainBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent
|
||||
|
||||
class AddNetworkMainFragment : BaseFragment<AddNetworkMainViewModel, FragmentAddNetworkMainBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_PAYLOAD = "key_payload"
|
||||
|
||||
fun getBundle(payload: AddNetworkPayload): Bundle {
|
||||
return Bundle().apply {
|
||||
putParcelable(KEY_PAYLOAD, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentAddNetworkMainBinding.inflate(layoutInflater)
|
||||
|
||||
override fun initViews() {
|
||||
binder.addNetworkMainToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
|
||||
val payload: AddNetworkPayload? = argumentOrNull(KEY_PAYLOAD)
|
||||
val adapter = AddNetworkMainPagerAdapter(this, payload)
|
||||
binder.addNetworkMainViewPager.adapter = adapter
|
||||
|
||||
if (payload == null) {
|
||||
binder.addNetworkMainTabLayout.setupWithViewPager2(binder.addNetworkMainViewPager, adapter::getPageTitle)
|
||||
} else {
|
||||
binder.addNetworkMainTabLayout.makeGone()
|
||||
}
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<SettingsFeatureComponent>(
|
||||
requireContext(),
|
||||
SettingsFeatureApi::class.java
|
||||
)
|
||||
.addNetworkMainFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: AddNetworkMainViewModel) {
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.AddNetworkPayload.Mode.Add.NetworkType
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.networkDetails.AddNetworkFragment
|
||||
|
||||
class AddNetworkMainPagerAdapter(
|
||||
private val fragment: Fragment,
|
||||
private val payloadForSinglePage: AddNetworkPayload?
|
||||
) : FragmentStateAdapter(fragment) {
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return if (payloadForSinglePage == null) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return if (payloadForSinglePage == null) {
|
||||
when (position) {
|
||||
0 -> AddNetworkFragment().addPayloadEmptyMode(NetworkType.SUBSTRATE)
|
||||
1 -> AddNetworkFragment().addPayloadEmptyMode(NetworkType.EVM)
|
||||
else -> throw IllegalArgumentException("Invalid position")
|
||||
}
|
||||
} else {
|
||||
AddNetworkFragment().addPayload(payloadForSinglePage)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPageTitle(position: Int): CharSequence {
|
||||
return if (payloadForSinglePage == null) {
|
||||
when (position) {
|
||||
0 -> fragment.getString(R.string.common_substrate)
|
||||
1 -> fragment.getString(R.string.common_evm)
|
||||
else -> throw IllegalArgumentException("Invalid position")
|
||||
}
|
||||
} else {
|
||||
"" // For single page we hide TabLayout
|
||||
}
|
||||
}
|
||||
|
||||
private fun AddNetworkFragment.addPayloadEmptyMode(networkType: NetworkType): AddNetworkFragment {
|
||||
return addPayload(AddNetworkPayload(AddNetworkPayload.Mode.Add(networkType, null)))
|
||||
}
|
||||
|
||||
private fun AddNetworkFragment.addPayload(mode: AddNetworkPayload): AddNetworkFragment {
|
||||
this.arguments = AddNetworkFragment.getBundle(mode)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
|
||||
class AddNetworkMainViewModel(
|
||||
private val router: SettingsRouter
|
||||
) : BaseViewModel() {
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.util.ChainParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class AddNetworkPayload(
|
||||
val mode: Mode,
|
||||
|
||||
) : Parcelable {
|
||||
|
||||
sealed interface Mode : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
class Add(val networkType: NetworkType, val chainParcel: ChainParcel?) : Mode {
|
||||
|
||||
enum class NetworkType {
|
||||
SUBSTRATE, EVM
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class Edit(val chainId: String) : Mode
|
||||
}
|
||||
}
|
||||
|
||||
fun AddNetworkPayload.Mode.Add.getChain(): Chain? {
|
||||
return chainParcel?.chain
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.AddNetworkMainFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
AddNetworkMainModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface AddNetworkMainComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment
|
||||
): AddNetworkMainComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: AddNetworkMainFragment)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.AddNetworkMainViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class AddNetworkMainModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(AddNetworkMainViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: SettingsRouter
|
||||
): ViewModel {
|
||||
return AddNetworkMainViewModel(
|
||||
router
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): AddNetworkMainViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(AddNetworkMainViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.networkDetails
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.view.isVisible
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.mixin.impl.observeValidations
|
||||
import io.novafoundation.nova.common.utils.bindTo
|
||||
import io.novafoundation.nova.common.view.setState
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.FragmentAddNetworkBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.AddNetworkPayload
|
||||
|
||||
class AddNetworkFragment : BaseFragment<AddNetworkViewModel, FragmentAddNetworkBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_PAYLOAD = "key_payload"
|
||||
|
||||
fun getBundle(payload: AddNetworkPayload): Bundle {
|
||||
return Bundle().apply {
|
||||
putParcelable(KEY_PAYLOAD, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentAddNetworkBinding.inflate(layoutInflater)
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<SettingsFeatureComponent>(
|
||||
requireContext(),
|
||||
SettingsFeatureApi::class.java
|
||||
)
|
||||
.addNetworkFactory()
|
||||
.create(this, argument(KEY_PAYLOAD))
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.addNetworkApplyButton.prepareForProgress(lifecycleOwner)
|
||||
|
||||
binder.addNetworkApplyButton.setOnClickListener { viewModel.addNetworkClicked() }
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: AddNetworkViewModel) {
|
||||
observeValidations(viewModel)
|
||||
viewModel.isNodeEditable.observe { binder.addNetworkNodeUrl.isEnabled = it }
|
||||
binder.addNetworkNodeUrl.bindTo(viewModel.nodeUrlFlow, viewModel)
|
||||
binder.addNetworkName.bindTo(viewModel.networkNameFlow, viewModel)
|
||||
binder.addNetworkCurrency.bindTo(viewModel.tokenSymbolFlow, viewModel)
|
||||
viewModel.isChainIdVisibleFlow.observe { binder.addNetworkChainIdContainer.isVisible = it }
|
||||
binder.addNetworkChainId.bindTo(viewModel.evmChainIdFlow, viewModel)
|
||||
binder.addNetworkBlockExplorer.bindTo(viewModel.blockExplorerFlow, viewModel)
|
||||
binder.addNetworkPriceInfoProvider.bindTo(viewModel.priceProviderFlow, viewModel)
|
||||
viewModel.buttonState.observe { binder.addNetworkApplyButton.setState(it) }
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.networkDetails
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser
|
||||
import io.novafoundation.nova.common.mixin.api.Validatable
|
||||
import io.novafoundation.nova.common.presentation.DescriptiveButtonState
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.utils.formatting.format
|
||||
import io.novafoundation.nova.common.utils.nullIfBlank
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.common.validation.progressConsumer
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.AddNetworkInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.model.CustomNetworkPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNetwork.CustomNetworkValidationSystem
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.AddNetworkPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.AddNetworkPayload.Mode
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.getChain
|
||||
import io.novafoundation.nova.runtime.ext.evmChainIdOrNull
|
||||
import io.novafoundation.nova.runtime.ext.networkType
|
||||
import io.novafoundation.nova.runtime.ext.normalizedUrl
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.NetworkType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class AddNetworkViewModel(
|
||||
private val resourceManager: ResourceManager,
|
||||
private val router: SettingsRouter,
|
||||
private val payload: AddNetworkPayload,
|
||||
private val interactor: AddNetworkInteractor,
|
||||
private val validationExecutor: ValidationExecutor,
|
||||
private val autofillNetworkMetadataMixinFactory: AutofillNetworkMetadataMixinFactory,
|
||||
private val coinGeckoLinkParser: CoinGeckoLinkParser,
|
||||
private val chainRegistry: ChainRegistry
|
||||
) : BaseViewModel(), Validatable by validationExecutor {
|
||||
|
||||
private val networkType = flowOf { getNetworkType() }
|
||||
.shareInBackground()
|
||||
|
||||
val isChainIdVisibleFlow = flowOf { isChainIdFieldRequired() }
|
||||
.shareInBackground()
|
||||
|
||||
val isNodeEditable = flowOf { payload.mode is Mode.Add }
|
||||
.shareInBackground()
|
||||
|
||||
val nodeUrlFlow = MutableStateFlow("")
|
||||
val networkNameFlow = MutableStateFlow("")
|
||||
val tokenSymbolFlow = MutableStateFlow("")
|
||||
val evmChainIdFlow = MutableStateFlow("")
|
||||
val blockExplorerFlow = MutableStateFlow("")
|
||||
val priceProviderFlow = MutableStateFlow("")
|
||||
|
||||
private val loadingState = MutableStateFlow(false)
|
||||
|
||||
val buttonState = combine(
|
||||
nodeUrlFlow,
|
||||
networkNameFlow,
|
||||
tokenSymbolFlow,
|
||||
evmChainIdFlow,
|
||||
loadingState
|
||||
) { url, networkName, tokenName, chainId, isLoading ->
|
||||
val chainIdRequiredAndEmpty = isChainIdFieldRequired() && chainId.isBlank()
|
||||
when {
|
||||
isLoading -> DescriptiveButtonState.Loading
|
||||
url.isBlank() || networkName.isBlank() || tokenName.isBlank() || chainIdRequiredAndEmpty -> DescriptiveButtonState.Disabled(
|
||||
resourceManager.getString(R.string.common_enter_details)
|
||||
)
|
||||
|
||||
else -> {
|
||||
val resId = when (payload.mode) {
|
||||
is Mode.Add -> R.string.common_add_network
|
||||
is Mode.Edit -> R.string.common_save
|
||||
}
|
||||
DescriptiveButtonState.Enabled(resourceManager.getString(resId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
launch {
|
||||
prefillData()
|
||||
|
||||
runAutofill()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prefillData() {
|
||||
val mode = payload.mode
|
||||
val chain = when (mode) {
|
||||
is Mode.Add -> mode.getChain()
|
||||
is Mode.Edit -> chainRegistry.getChain(mode.chainId)
|
||||
}
|
||||
|
||||
val asset = chain?.utilityAsset
|
||||
nodeUrlFlow.value = chain?.nodes?.nodes?.first()?.unformattedUrl.orEmpty()
|
||||
networkNameFlow.value = chain?.name.orEmpty()
|
||||
tokenSymbolFlow.value = asset?.symbol?.value.orEmpty()
|
||||
evmChainIdFlow.value = chain?.evmChainIdOrNull()?.format().orEmpty()
|
||||
blockExplorerFlow.value = chain?.explorers?.firstOrNull()?.normalizedUrl().orEmpty()
|
||||
priceProviderFlow.value = asset?.priceId?.let { coinGeckoLinkParser.format(it) }.orEmpty()
|
||||
}
|
||||
|
||||
fun addNetworkClicked() {
|
||||
launch {
|
||||
val chainName = networkNameFlow.value
|
||||
val blockExplorerUrl = blockExplorerFlow.value.nullIfBlank()
|
||||
val blockExplorerName = resourceManager.getString(R.string.create_network_block_explorer_name, chainName)
|
||||
val validationPayload = CustomNetworkPayload(
|
||||
nodeUrl = nodeUrlFlow.value,
|
||||
nodeName = resourceManager.getString(R.string.create_network_node_name, chainName),
|
||||
chainName = chainName,
|
||||
tokenSymbol = tokenSymbolFlow.value,
|
||||
evmChainId = evmChainIdFlow.value.toIntOrNull(),
|
||||
blockExplorer = blockExplorerUrl?.let { CustomNetworkPayload.BlockExplorer(blockExplorerName, it) },
|
||||
coingeckoLinkUrl = priceProviderFlow.value.nullIfBlank(),
|
||||
ignoreChainModifying = payload.mode is Mode.Edit // Skip dialog about Chain modifying if it's already editing
|
||||
)
|
||||
|
||||
validationExecutor.requireValid(
|
||||
validationSystem = getValidationSystem(),
|
||||
payload = validationPayload,
|
||||
progressConsumer = loadingState.progressConsumer(),
|
||||
validationFailureTransformerCustom = { status, actions ->
|
||||
mapSaveCustomNetworkFailureToUI(
|
||||
resourceManager,
|
||||
status,
|
||||
actions,
|
||||
::changeTokenSymbol
|
||||
)
|
||||
}
|
||||
) {
|
||||
executeSaving(validationPayload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeSaving(savingPayload: CustomNetworkPayload) {
|
||||
launch {
|
||||
val result = when (payload.mode) {
|
||||
is Mode.Add -> createNetwork(payload.mode, savingPayload)
|
||||
is Mode.Edit -> editNetwork(payload.mode, savingPayload)
|
||||
}
|
||||
|
||||
result.onSuccess { finishSavingFlow() }
|
||||
.onFailure {
|
||||
Log.e(LOG_TAG, "Failed to save network", it)
|
||||
showError(resourceManager.getString(R.string.common_something_went_wrong_title))
|
||||
}
|
||||
|
||||
loadingState.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishSavingFlow() {
|
||||
launch {
|
||||
when (payload.mode) {
|
||||
is Mode.Add -> router.finishCreateNetworkFlow()
|
||||
is Mode.Edit -> router.back()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createNetwork(mode: Mode.Add, savingPayload: CustomNetworkPayload): Result<Unit> {
|
||||
return when (mode.networkType) {
|
||||
Mode.Add.NetworkType.EVM -> interactor.createEvmNetwork(savingPayload, mode.getChain())
|
||||
|
||||
Mode.Add.NetworkType.SUBSTRATE -> interactor.createSubstrateNetwork(savingPayload, mode.getChain(), coroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun editNetwork(mode: Mode.Edit, savingPayload: CustomNetworkPayload): Result<Unit> {
|
||||
return interactor.updateChain(
|
||||
mode.chainId,
|
||||
savingPayload.chainName,
|
||||
savingPayload.tokenSymbol,
|
||||
savingPayload.blockExplorer,
|
||||
savingPayload.coingeckoLinkUrl
|
||||
)
|
||||
}
|
||||
|
||||
private fun changeTokenSymbol(symbol: String) {
|
||||
tokenSymbolFlow.value = symbol
|
||||
}
|
||||
|
||||
private suspend fun getValidationSystem(): CustomNetworkValidationSystem {
|
||||
return when (networkType.first()) {
|
||||
NetworkType.EVM -> interactor.getEvmValidationSystem(viewModelScope)
|
||||
NetworkType.SUBSTRATE -> interactor.getSubstrateValidationSystem(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runAutofill() {
|
||||
launch {
|
||||
val autofillMixin = when (networkType.first()) {
|
||||
NetworkType.EVM -> autofillNetworkMetadataMixinFactory.evm(viewModelScope)
|
||||
NetworkType.SUBSTRATE -> autofillNetworkMetadataMixinFactory.substrate(viewModelScope)
|
||||
}
|
||||
|
||||
nodeUrlFlow
|
||||
.ignorePrefilledValues()
|
||||
.debounce(500.milliseconds)
|
||||
.mapLatest { url -> autofillMixin.autofill(url) }
|
||||
.onEach { result ->
|
||||
result.onSuccess { data ->
|
||||
data.chainName?.let { networkNameFlow.value = it }
|
||||
data.tokenSymbol?.let { tokenSymbolFlow.value = it }
|
||||
data.evmChainId?.let { evmChainIdFlow.value = it.toString() }
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Flow<T>.ignorePrefilledValues(): Flow<T> {
|
||||
return this.drop(1)
|
||||
}
|
||||
|
||||
private fun isChainIdFieldRequired(): Boolean {
|
||||
return payload.mode is Mode.Add && payload.mode.networkType == Mode.Add.NetworkType.EVM
|
||||
}
|
||||
|
||||
private suspend fun getNetworkType(): NetworkType {
|
||||
return when (payload.mode) {
|
||||
is Mode.Add -> {
|
||||
when (payload.mode.networkType) {
|
||||
Mode.Add.NetworkType.SUBSTRATE -> NetworkType.SUBSTRATE
|
||||
Mode.Add.NetworkType.EVM -> NetworkType.EVM
|
||||
}
|
||||
}
|
||||
|
||||
is Mode.Edit -> chainRegistry.getChain(payload.mode.chainId).networkType()
|
||||
}
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.networkDetails
|
||||
|
||||
import io.novafoundation.nova.common.data.network.runtime.calls.GetChainRequest
|
||||
import io.novafoundation.nova.common.data.network.runtime.calls.GetSystemPropertiesRequest
|
||||
import io.novafoundation.nova.common.data.network.runtime.model.SystemProperties
|
||||
import io.novafoundation.nova.common.data.network.runtime.model.firstTokenSymbol
|
||||
import io.novafoundation.nova.core.ethereum.Web3Api
|
||||
import io.novafoundation.nova.runtime.ethereum.Web3ApiFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.sendSuspend
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnection
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory
|
||||
import io.novasama.substrate_sdk_android.wsrpc.executeAsync
|
||||
import io.novasama.substrate_sdk_android.wsrpc.mappers.nonNull
|
||||
import io.novasama.substrate_sdk_android.wsrpc.mappers.pojo
|
||||
import java.math.BigInteger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class AutofillNetworkData(
|
||||
val chainName: String?,
|
||||
val tokenSymbol: String?,
|
||||
val evmChainId: BigInteger?
|
||||
)
|
||||
|
||||
class AutofillNetworkMetadataMixinFactory(
|
||||
private val nodeConnectionFactory: NodeConnectionFactory,
|
||||
private val web3ApiFactory: Web3ApiFactory
|
||||
) {
|
||||
fun substrate(coroutineScope: CoroutineScope): SubstrateAutofillNetworkMetadataMixin {
|
||||
return SubstrateAutofillNetworkMetadataMixin(nodeConnectionFactory, coroutineScope)
|
||||
}
|
||||
|
||||
fun evm(coroutineScope: CoroutineScope): EvmAutofillNetworkMetadataMixin {
|
||||
return EvmAutofillNetworkMetadataMixin(nodeConnectionFactory, coroutineScope, web3ApiFactory)
|
||||
}
|
||||
}
|
||||
|
||||
interface AutofillNetworkMetadataMixin {
|
||||
|
||||
suspend fun autofill(url: String): Result<AutofillNetworkData>
|
||||
}
|
||||
|
||||
class SubstrateAutofillNetworkMetadataMixin(
|
||||
private val nodeConnectionFactory: NodeConnectionFactory,
|
||||
private val coroutineScope: CoroutineScope
|
||||
) : AutofillNetworkMetadataMixin {
|
||||
|
||||
private var nodeConnection: NodeConnection? = null
|
||||
|
||||
override suspend fun autofill(url: String): Result<AutofillNetworkData> = runCatching {
|
||||
if (nodeConnection == null) {
|
||||
nodeConnection = nodeConnectionFactory.createNodeConnection(url, coroutineScope)
|
||||
} else {
|
||||
nodeConnection!!.switchUrl(url)
|
||||
}
|
||||
|
||||
val properties = getSubstrateChainProperties(nodeConnection!!)
|
||||
val chainName = getSubstrateChainName(nodeConnection!!)
|
||||
|
||||
AutofillNetworkData(
|
||||
chainName = chainName,
|
||||
tokenSymbol = properties.firstTokenSymbol(),
|
||||
evmChainId = null
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getSubstrateChainProperties(nodeConnection: NodeConnection): SystemProperties {
|
||||
return nodeConnection.getSocketService()
|
||||
.executeAsync(GetSystemPropertiesRequest(), mapper = pojo<SystemProperties>().nonNull())
|
||||
}
|
||||
|
||||
private suspend fun getSubstrateChainName(nodeConnection: NodeConnection): String {
|
||||
return nodeConnection.getSocketService()
|
||||
.executeAsync(GetChainRequest(), mapper = pojo<String>().nonNull())
|
||||
}
|
||||
}
|
||||
|
||||
class EvmAutofillNetworkMetadataMixin(
|
||||
private val nodeConnectionFactory: NodeConnectionFactory,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val web3ApiFactory: Web3ApiFactory
|
||||
) : AutofillNetworkMetadataMixin {
|
||||
|
||||
private var nodeConnection: NodeConnection? = null
|
||||
private var web3Api: Web3Api? = null
|
||||
|
||||
override suspend fun autofill(url: String): Result<AutofillNetworkData> = runCatching {
|
||||
if (nodeConnection == null) {
|
||||
nodeConnection = nodeConnectionFactory.createNodeConnection(url, coroutineScope)
|
||||
} else {
|
||||
nodeConnection!!.switchUrl(url)
|
||||
}
|
||||
|
||||
if (web3Api == null) {
|
||||
web3Api = web3ApiFactory.createWss(nodeConnection!!.getSocketService())
|
||||
}
|
||||
|
||||
val chainId = web3Api!!.ethChainId().sendSuspend().chainId
|
||||
|
||||
AutofillNetworkData(
|
||||
chainName = null,
|
||||
tokenSymbol = null,
|
||||
evmChainId = chainId
|
||||
)
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.networkDetails
|
||||
|
||||
import io.novafoundation.nova.common.mixin.api.CustomDialogDisplayer
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.validation.TransformedFailure
|
||||
import io.novafoundation.nova.common.validation.TransformedFailure.Default
|
||||
import io.novafoundation.nova.common.validation.ValidationFlowActions
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNetwork.CustomNetworkFailure
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.model.CustomNetworkPayload
|
||||
|
||||
fun mapSaveCustomNetworkFailureToUI(
|
||||
resourceManager: ResourceManager,
|
||||
status: ValidationStatus.NotValid<CustomNetworkFailure>,
|
||||
actions: ValidationFlowActions<CustomNetworkPayload>,
|
||||
changeTokenSymbolAction: (String) -> Unit
|
||||
): TransformedFailure {
|
||||
return when (val reason = status.reason) {
|
||||
is CustomNetworkFailure.DefaultNetworkAlreadyAdded -> Default(
|
||||
resourceManager.getString(R.string.network_already_exist_failure_title) to
|
||||
resourceManager.getString(R.string.default_network_already_exist_failure_message, reason.networkName)
|
||||
)
|
||||
|
||||
is CustomNetworkFailure.CustomNetworkAlreadyAdded -> {
|
||||
TransformedFailure.Custom(
|
||||
CustomDialogDisplayer.Payload(
|
||||
title = resourceManager.getString(R.string.network_already_exist_failure_title),
|
||||
message = resourceManager.getString(R.string.custom_network_already_exist_failure_message, reason.networkName),
|
||||
cancelAction = CustomDialogDisplayer.Payload.DialogAction(
|
||||
title = resourceManager.getString(R.string.common_close),
|
||||
action = { }
|
||||
),
|
||||
okAction = CustomDialogDisplayer.Payload.DialogAction(
|
||||
title = resourceManager.getString(R.string.common_modify),
|
||||
action = {
|
||||
actions.revalidate { payload ->
|
||||
payload.copy(ignoreChainModifying = true)
|
||||
}
|
||||
}
|
||||
),
|
||||
customStyle = R.style.AccentAlertDialogTheme
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
CustomNetworkFailure.NodeIsNotAlive -> Default(
|
||||
resourceManager.getString(R.string.node_not_alive_title) to
|
||||
resourceManager.getString(R.string.node_not_alive_message)
|
||||
)
|
||||
|
||||
is CustomNetworkFailure.WrongNetwork -> Default(
|
||||
resourceManager.getString(R.string.create_network_invalid_chain_id_title) to
|
||||
resourceManager.getString(R.string.create_network_invalid_chain_id_message)
|
||||
)
|
||||
|
||||
CustomNetworkFailure.CoingeckoLinkBadFormat -> Default(
|
||||
resourceManager.getString(R.string.asset_add_evm_token_invalid_coin_gecko_link_title) to
|
||||
resourceManager.getString(R.string.asset_add_evm_token_invalid_coin_gecko_link_message)
|
||||
)
|
||||
|
||||
is CustomNetworkFailure.WrongAsset -> {
|
||||
TransformedFailure.Custom(
|
||||
CustomDialogDisplayer.Payload(
|
||||
title = resourceManager.getString(R.string.invalid_token_symbol_title),
|
||||
message = resourceManager.getString(R.string.invalid_token_symbol_message, reason.usedSymbol, reason.correctSymbol),
|
||||
cancelAction = CustomDialogDisplayer.Payload.DialogAction(
|
||||
title = resourceManager.getString(R.string.common_close),
|
||||
action = { }
|
||||
),
|
||||
okAction = CustomDialogDisplayer.Payload.DialogAction(
|
||||
title = resourceManager.getString(R.string.common_apply),
|
||||
action = {
|
||||
changeTokenSymbolAction(reason.correctSymbol)
|
||||
}
|
||||
),
|
||||
customStyle = R.style.AccentAlertDialogTheme
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.networkDetails.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.AddNetworkPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.networkDetails.AddNetworkFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
AddNetworkModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface AddNetworkComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance argument: AddNetworkPayload,
|
||||
): AddNetworkComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: AddNetworkFragment)
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.networkDetails.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.AddNetworkInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.AddNetworkPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.networkDetails.AddNetworkViewModel
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.networkDetails.AutofillNetworkMetadataMixinFactory
|
||||
import io.novafoundation.nova.runtime.ethereum.Web3ApiFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.connection.node.connection.NodeConnectionFactory
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class AddNetworkModule {
|
||||
|
||||
@Provides
|
||||
fun provideAutofillNetworkMetadataMixinFactory(
|
||||
nodeConnectionFactory: NodeConnectionFactory,
|
||||
web3ApiFactory: Web3ApiFactory
|
||||
): AutofillNetworkMetadataMixinFactory {
|
||||
return AutofillNetworkMetadataMixinFactory(
|
||||
nodeConnectionFactory,
|
||||
web3ApiFactory
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(AddNetworkViewModel::class)
|
||||
fun provideViewModel(
|
||||
resourceManager: ResourceManager,
|
||||
router: SettingsRouter,
|
||||
payload: AddNetworkPayload,
|
||||
interactor: AddNetworkInteractor,
|
||||
validationExecutor: ValidationExecutor,
|
||||
autofillNetworkMetadataMixinFactory: AutofillNetworkMetadataMixinFactory,
|
||||
coinGeckoLinkParser: CoinGeckoLinkParser,
|
||||
chainRegistry: ChainRegistry,
|
||||
): ViewModel {
|
||||
return AddNetworkViewModel(
|
||||
resourceManager = resourceManager,
|
||||
router = router,
|
||||
payload = payload,
|
||||
interactor = interactor,
|
||||
validationExecutor = validationExecutor,
|
||||
autofillNetworkMetadataMixinFactory = autofillNetworkMetadataMixinFactory,
|
||||
coinGeckoLinkParser = coinGeckoLinkParser,
|
||||
chainRegistry = chainRegistry
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): AddNetworkViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(AddNetworkViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.ViewSpace
|
||||
import io.novafoundation.nova.common.view.dialog.warningDialog
|
||||
import io.novafoundation.nova.common.view.input.selector.setupListSelectorMixin
|
||||
import io.novafoundation.nova.common.view.recyclerview.adapter.text.TextAdapter
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.FragmentChainNetworkManagementBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.headerAdapter.ChainNetworkManagementHeaderAdapter
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.ChainNetworkManagementNodesAdapter
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.NodesItemBackgroundDecoration
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.NodesItemDividerDecoration
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.items.NetworkNodeRvItem
|
||||
import javax.inject.Inject
|
||||
|
||||
private val NODES_GROUP_TEXT_STYLE = R.style.TextAppearance_NovaFoundation_SemiBold_Caps2
|
||||
private val NODES_GROUP_COLOR_RES = R.color.text_secondary
|
||||
private val NODES_GROUP_TEXT_PADDING_DP = ViewSpace(16, 24, 16, 0)
|
||||
|
||||
class ChainNetworkManagementFragment :
|
||||
BaseFragment<ChainNetworkManagementViewModel, FragmentChainNetworkManagementBinding>(),
|
||||
ChainNetworkManagementHeaderAdapter.ItemHandler,
|
||||
ChainNetworkManagementNodesAdapter.ItemHandler {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_PAYLOAD = "payload"
|
||||
|
||||
fun getBundle(payload: ChainNetworkManagementPayload): Bundle {
|
||||
return Bundle().apply {
|
||||
putParcelable(EXTRA_PAYLOAD, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentChainNetworkManagementBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val headerAdapter by lazy { ChainNetworkManagementHeaderAdapter(imageLoader, this) }
|
||||
private val customNodesTitleAdapter by lazy {
|
||||
TextAdapter(
|
||||
requireContext().getString(R.string.network_management_custom_nodes),
|
||||
NODES_GROUP_TEXT_STYLE,
|
||||
NODES_GROUP_COLOR_RES,
|
||||
NODES_GROUP_TEXT_PADDING_DP
|
||||
)
|
||||
}
|
||||
private val customNodes by lazy { ChainNetworkManagementNodesAdapter(this) }
|
||||
private val defaultNodesTitleAdapter by lazy {
|
||||
TextAdapter(
|
||||
requireContext().getString(R.string.network_management_default_nodes),
|
||||
NODES_GROUP_TEXT_STYLE,
|
||||
NODES_GROUP_COLOR_RES,
|
||||
NODES_GROUP_TEXT_PADDING_DP
|
||||
)
|
||||
}
|
||||
private val defaultNodes by lazy { ChainNetworkManagementNodesAdapter(this) }
|
||||
|
||||
private val adapter by lazy {
|
||||
ConcatAdapter(
|
||||
headerAdapter,
|
||||
customNodesTitleAdapter,
|
||||
customNodes,
|
||||
defaultNodesTitleAdapter,
|
||||
defaultNodes
|
||||
)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.chainNetworkManagementToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
binder.chainNetworkManagementToolbar.setRightActionClickListener { viewModel.networkActionsClicked() }
|
||||
|
||||
binder.chainNetworkManagementContent.adapter = adapter
|
||||
binder.chainNetworkManagementContent.itemAnimator = null
|
||||
binder.chainNetworkManagementContent.addItemDecoration(NodesItemBackgroundDecoration(requireContext()))
|
||||
binder.chainNetworkManagementContent.addItemDecoration(NodesItemDividerDecoration(requireContext()))
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<SettingsFeatureComponent>(
|
||||
requireContext(),
|
||||
SettingsFeatureApi::class.java
|
||||
)
|
||||
.chainNetworkManagementFactory()
|
||||
.create(this, argument(EXTRA_PAYLOAD))
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: ChainNetworkManagementViewModel) {
|
||||
setupListSelectorMixin(viewModel.listSelectorMixin)
|
||||
|
||||
viewModel.isNetworkEditable.observe {
|
||||
if (it) {
|
||||
binder.chainNetworkManagementToolbar.setRightActionTint(R.color.icon_primary)
|
||||
binder.chainNetworkManagementToolbar.setRightIconRes(R.drawable.ic_more_horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isNetworkCanBeDisabled.observe {
|
||||
headerAdapter.setNetworkCanBeDisabled(it)
|
||||
}
|
||||
|
||||
viewModel.chainEnabled.observe {
|
||||
headerAdapter.setChainEnabled(it)
|
||||
}
|
||||
|
||||
viewModel.autoBalanceEnabled.observe {
|
||||
headerAdapter.setAutoBalanceEnabled(it)
|
||||
}
|
||||
|
||||
viewModel.chainModel.observe {
|
||||
headerAdapter.setChainUiModel(it)
|
||||
}
|
||||
|
||||
viewModel.customNodes.observe {
|
||||
customNodes.submitList(it)
|
||||
}
|
||||
|
||||
viewModel.defaultNodes.observe {
|
||||
defaultNodesTitleAdapter.show(it.isNotEmpty())
|
||||
defaultNodes.submitList(it)
|
||||
}
|
||||
|
||||
viewModel.confirmAccountDeletion.awaitableActionLiveData.observeEvent {
|
||||
warningDialog(
|
||||
requireContext(),
|
||||
onPositiveClick = { it.onSuccess(true) },
|
||||
onNegativeClick = { it.onSuccess(false) },
|
||||
positiveTextRes = R.string.common_delete
|
||||
) {
|
||||
setTitle(it.payload.first)
|
||||
setMessage(it.payload.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun chainEnableClicked() {
|
||||
viewModel.chainEnableClicked()
|
||||
}
|
||||
|
||||
override fun autoBalanceClicked() {
|
||||
viewModel.autoBalanceClicked()
|
||||
}
|
||||
|
||||
override fun selectNode(item: NetworkNodeRvItem) {
|
||||
viewModel.selectNode(item)
|
||||
}
|
||||
|
||||
override fun editNode(item: NetworkNodeRvItem) {
|
||||
viewModel.nodeActionClicked(item)
|
||||
}
|
||||
|
||||
override fun addNewNode() {
|
||||
viewModel.addNewNode()
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class ChainNetworkManagementPayload(
|
||||
val chainId: String
|
||||
) : Parcelable
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingOrDenyingAction
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.view.input.selector.ListSelectorMixin
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.ChainNetworkState
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.NetworkManagementChainInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.NodeHealthState
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.AddNetworkPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.items.NetworkConnectionRvItem
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.items.NetworkNodeRvItem
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.items.NetworkNodesAddCustomRvItem
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.common.ConnectionStateModel
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node.CustomNodePayload
|
||||
import io.novafoundation.nova.runtime.ext.autoBalanceEnabled
|
||||
import io.novafoundation.nova.runtime.ext.isCustomNetwork
|
||||
import io.novafoundation.nova.runtime.ext.isEnabled
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChainNetworkManagementViewModel(
|
||||
private val router: SettingsRouter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val networkManagementChainInteractor: NetworkManagementChainInteractor,
|
||||
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
private val listSelectorMixinFactory: ListSelectorMixin.Factory,
|
||||
private val payload: ChainNetworkManagementPayload
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val chainNetworkStateFlow = networkManagementChainInteractor.chainStateFlow(payload.chainId, viewModelScope)
|
||||
.shareInBackground()
|
||||
|
||||
val isNetworkCanBeDisabled: Flow<Boolean> = chainNetworkStateFlow.map { it.networkCanBeDisabled }
|
||||
val chainEnabled: Flow<Boolean> = chainNetworkStateFlow.map { it.chain.isEnabled }
|
||||
val autoBalanceEnabled: Flow<Boolean> = chainNetworkStateFlow.map { it.chain.autoBalanceEnabled }
|
||||
val chainModel: Flow<ChainUi> = chainNetworkStateFlow.map { mapChainToUi(it.chain) }
|
||||
|
||||
val isNetworkEditable = chainNetworkStateFlow.map { it.chain.isCustomNetwork }
|
||||
|
||||
val customNodes: Flow<List<NetworkConnectionRvItem>> = chainNetworkStateFlow.map { chainNetworkState ->
|
||||
buildList {
|
||||
val nodes = chainNetworkState.nodeHealthStates.filter { it.node.isCustom }
|
||||
.map { mapNodeToUi(it, chainNetworkState) }
|
||||
|
||||
add(NetworkNodesAddCustomRvItem())
|
||||
addAll(nodes)
|
||||
}
|
||||
}
|
||||
|
||||
val defaultNodes: Flow<List<NetworkConnectionRvItem>> = chainNetworkStateFlow.map { chainNetworkState ->
|
||||
chainNetworkState.nodeHealthStates.filter { !it.node.isCustom }
|
||||
.map { mapNodeToUi(it, chainNetworkState) }
|
||||
}
|
||||
|
||||
val confirmAccountDeletion = actionAwaitableMixinFactory.confirmingOrDenyingAction<Pair<String, String>>()
|
||||
|
||||
val listSelectorMixin = listSelectorMixinFactory.create(viewModelScope)
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun chainEnableClicked() {
|
||||
launch {
|
||||
networkManagementChainInteractor.toggleChainEnableState(payload.chainId)
|
||||
}
|
||||
}
|
||||
|
||||
fun autoBalanceClicked() {
|
||||
launch {
|
||||
networkManagementChainInteractor.toggleAutoBalance(payload.chainId)
|
||||
}
|
||||
}
|
||||
|
||||
fun selectNode(item: NetworkNodeRvItem) {
|
||||
launch {
|
||||
networkManagementChainInteractor.selectNode(payload.chainId, item.unformattedUrl)
|
||||
}
|
||||
}
|
||||
|
||||
fun nodeActionClicked(item: NetworkNodeRvItem) {
|
||||
launch {
|
||||
val state = chainNetworkStateFlow.first()
|
||||
val nodes = state.chain.nodes.nodes
|
||||
if (nodes.size > 1) {
|
||||
listSelectorMixin.showSelector(
|
||||
R.string.manage_node_actions_title,
|
||||
subtitle = item.name,
|
||||
listOf(
|
||||
editItem(R.string.manage_node_action_edit) { editNode(item.unformattedUrl) },
|
||||
deleteItem(R.string.manage_node_action_delete) { deleteNode(item) }
|
||||
)
|
||||
)
|
||||
} else {
|
||||
editNode(item.unformattedUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addNewNode() {
|
||||
router.openCustomNode(CustomNodePayload(payload.chainId, CustomNodePayload.Mode.Add))
|
||||
}
|
||||
|
||||
fun networkActionsClicked() {
|
||||
listSelectorMixin.showSelector(
|
||||
R.string.manage_network_actions_title,
|
||||
listOf(
|
||||
editItem(R.string.manage_network_action_edit, ::editNetwork),
|
||||
deleteItem(R.string.manage_network_action_delete, ::deleteNetwork)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun editNetwork() {
|
||||
router.openEditNetwork(AddNetworkPayload.Mode.Edit(payload.chainId))
|
||||
}
|
||||
|
||||
private fun deleteNetwork() {
|
||||
launch {
|
||||
confirmAccountDeletion.awaitAction(
|
||||
Pair(
|
||||
resourceManager.getString(R.string.manage_network_delete_title),
|
||||
resourceManager.getString(R.string.manage_network_delete_message)
|
||||
)
|
||||
)
|
||||
|
||||
networkManagementChainInteractor.deleteNetwork(payload.chainId)
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
|
||||
private fun editNode(nodeUrl: String) {
|
||||
router.openCustomNode(CustomNodePayload(payload.chainId, CustomNodePayload.Mode.Edit(nodeUrl)))
|
||||
}
|
||||
|
||||
private fun deleteNode(item: NetworkNodeRvItem) {
|
||||
launch {
|
||||
confirmAccountDeletion.awaitAction(
|
||||
Pair(
|
||||
resourceManager.getString(R.string.manage_network_delete_title),
|
||||
resourceManager.getString(R.string.manage_network_delete_message, item.name)
|
||||
)
|
||||
)
|
||||
|
||||
networkManagementChainInteractor.deleteNode(chainId = payload.chainId, unformattedNodeUrl = item.unformattedUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun editItem(textRes: Int, action: () -> Unit): ListSelectorMixin.Item {
|
||||
return ListSelectorMixin.Item(
|
||||
R.drawable.ic_pencil_edit,
|
||||
R.color.icon_primary,
|
||||
textRes,
|
||||
R.color.text_primary,
|
||||
action
|
||||
)
|
||||
}
|
||||
|
||||
private fun deleteItem(textRes: Int, action: () -> Unit): ListSelectorMixin.Item {
|
||||
return ListSelectorMixin.Item(
|
||||
R.drawable.ic_pencil_edit,
|
||||
R.color.icon_negative,
|
||||
textRes,
|
||||
R.color.text_negative,
|
||||
action
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapNodeToUi(nodeHealthState: NodeHealthState, networkState: ChainNetworkState): NetworkNodeRvItem {
|
||||
val selectingAvailable = !networkState.chain.autoBalanceEnabled && networkState.chain.isEnabled
|
||||
|
||||
return NetworkNodeRvItem(
|
||||
id = nodeHealthState.node.unformattedUrl,
|
||||
name = nodeHealthState.node.name,
|
||||
unformattedUrl = nodeHealthState.node.unformattedUrl,
|
||||
isEditable = nodeHealthState.node.isCustom,
|
||||
isDeletable = networkState.nodeHealthStates.size > 1 && nodeHealthState.node.isCustom,
|
||||
isSelected = nodeHealthState.node.unformattedUrl == networkState.connectingNode?.unformattedUrl,
|
||||
connectionState = mapConnectionStateToUi(nodeHealthState),
|
||||
isSelectable = selectingAvailable,
|
||||
nameColorRes = if (selectingAvailable) R.color.text_primary else R.color.text_secondary
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapConnectionStateToUi(nodeHealthState: NodeHealthState): ConnectionStateModel {
|
||||
return when (val state = nodeHealthState.state) {
|
||||
NodeHealthState.State.Connecting -> ConnectionStateModel(
|
||||
name = resourceManager.getString(R.string.common_connecting),
|
||||
chainStatusColor = resourceManager.getColor(R.color.text_secondary),
|
||||
chainStatusIcon = R.drawable.ic_connection_status_connecting,
|
||||
chainStatusIconColor = resourceManager.getColor(R.color.icon_secondary),
|
||||
showShimmering = true
|
||||
)
|
||||
|
||||
is NodeHealthState.State.Connected -> {
|
||||
val (iconRes, textColorRes) = when {
|
||||
state.ms < 99 -> R.drawable.ic_connection_status_good to R.color.text_positive
|
||||
state.ms < 499 -> R.drawable.ic_connection_status_average to R.color.text_warning
|
||||
else -> R.drawable.ic_connection_status_bad to R.color.text_negative
|
||||
}
|
||||
|
||||
ConnectionStateModel(
|
||||
name = resourceManager.getString(R.string.common_connected_ms, state.ms),
|
||||
chainStatusColor = resourceManager.getColor(textColorRes),
|
||||
chainStatusIcon = iconRes,
|
||||
chainStatusIconColor = null,
|
||||
showShimmering = false
|
||||
)
|
||||
}
|
||||
|
||||
NodeHealthState.State.Disabled -> ConnectionStateModel(
|
||||
name = null,
|
||||
chainStatusColor = null,
|
||||
chainStatusIcon = R.drawable.ic_connection_status_connecting,
|
||||
chainStatusIconColor = resourceManager.getColor(R.color.icon_inactive),
|
||||
showShimmering = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.ChainNetworkManagementFragment
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.ChainNetworkManagementPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
ChainNetworkManagementModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface ChainNetworkManagementComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: ChainNetworkManagementPayload
|
||||
): ChainNetworkManagementComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: ChainNetworkManagementFragment)
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.view.input.selector.ListSelectorMixin
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.NetworkManagementChainInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.ChainNetworkManagementPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.ChainNetworkManagementViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class ChainNetworkManagementModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(ChainNetworkManagementViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: SettingsRouter,
|
||||
resourceManager: ResourceManager,
|
||||
networkManagementChainInteractor: NetworkManagementChainInteractor,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
listSelectorMixinFactory: ListSelectorMixin.Factory,
|
||||
payload: ChainNetworkManagementPayload
|
||||
): ViewModel {
|
||||
return ChainNetworkManagementViewModel(
|
||||
router,
|
||||
resourceManager,
|
||||
networkManagementChainInteractor,
|
||||
actionAwaitableMixinFactory,
|
||||
listSelectorMixinFactory,
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): ChainNetworkManagementViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(ChainNetworkManagementViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.headerAdapter
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.list.SingleItemAdapter
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.ChainUi
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.loadChainIcon
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.ItemChanNetworkManagementHeaderBinding
|
||||
|
||||
class ChainNetworkManagementHeaderAdapter(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val itemHandler: ItemHandler
|
||||
) : SingleItemAdapter<ChainNetworkManagementHeaderViewHolder>(isShownByDefault = true) {
|
||||
|
||||
private var chainUiModel: ChainUi? = null
|
||||
private var chainEnabled = false
|
||||
private var autoBalanceEnabled = false
|
||||
private var networkCanBeDisabled = false
|
||||
|
||||
interface ItemHandler {
|
||||
|
||||
fun chainEnableClicked()
|
||||
|
||||
fun autoBalanceClicked()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChainNetworkManagementHeaderViewHolder {
|
||||
return ChainNetworkManagementHeaderViewHolder(
|
||||
ItemChanNetworkManagementHeaderBinding.inflate(parent.inflater(), parent, false),
|
||||
imageLoader,
|
||||
itemHandler
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ChainNetworkManagementHeaderViewHolder, position: Int) {
|
||||
chainUiModel?.let { holder.bind(it, chainEnabled, autoBalanceEnabled, networkCanBeDisabled) }
|
||||
}
|
||||
|
||||
fun setChainUiModel(chainUiModel: ChainUi) {
|
||||
this.chainUiModel = chainUiModel
|
||||
notifyItemChanged(0)
|
||||
}
|
||||
|
||||
fun setChainEnabled(chainEnabled: Boolean) {
|
||||
this.chainEnabled = chainEnabled
|
||||
notifyItemChanged(0)
|
||||
}
|
||||
|
||||
fun setAutoBalanceEnabled(autoBalanceEnabled: Boolean) {
|
||||
this.autoBalanceEnabled = autoBalanceEnabled
|
||||
notifyItemChanged(0)
|
||||
}
|
||||
|
||||
fun setNetworkCanBeDisabled(networkCanBeDisabled: Boolean) {
|
||||
this.networkCanBeDisabled = networkCanBeDisabled
|
||||
}
|
||||
}
|
||||
|
||||
class ChainNetworkManagementHeaderViewHolder(
|
||||
private val binder: ItemChanNetworkManagementHeaderBinding,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val itemHandler: ChainNetworkManagementHeaderAdapter.ItemHandler
|
||||
) : RecyclerView.ViewHolder(binder.root) {
|
||||
|
||||
init {
|
||||
with(binder) {
|
||||
chainNetworkManagementEnable.setOnClickListener { itemHandler.chainEnableClicked() }
|
||||
chainNetworkManagementAutoBalance.setOnClickListener { itemHandler.autoBalanceClicked() }
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(chainUi: ChainUi, chainEnabled: Boolean, autoBalanceEnabled: Boolean, networkCanBeDisabled: Boolean) {
|
||||
with(binder) {
|
||||
chainNetworkManagementIcon.loadChainIcon(chainUi.icon, imageLoader)
|
||||
chainNetworkManagementTitle.text = chainUi.name
|
||||
|
||||
chainNetworkManagementEnable.setChecked(chainEnabled)
|
||||
chainNetworkManagementEnable.isEnabled = networkCanBeDisabled
|
||||
|
||||
chainNetworkManagementAutoBalance.setChecked(autoBalanceEnabled)
|
||||
chainNetworkManagementAutoBalance.isEnabled = chainEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setCompoundDrawableTint
|
||||
import io.novafoundation.nova.common.utils.setDrawableStart
|
||||
import io.novafoundation.nova.common.utils.setShimmerShown
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.common.view.shape.getMaskedRipple
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.ItemChanNetworkManagementAddNodeButtonBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.ItemChanNetworkManagementNodeBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.items.NetworkConnectionRvItem
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.items.NetworkNodeRvItem
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.items.NetworkNodesAddCustomRvItem
|
||||
|
||||
class ChainNetworkManagementNodesAdapter(
|
||||
private val itemHandler: ItemHandler
|
||||
) : ListAdapter<NetworkConnectionRvItem, ViewHolder>(DiffCallback()) {
|
||||
|
||||
interface ItemHandler {
|
||||
|
||||
fun selectNode(item: NetworkNodeRvItem)
|
||||
|
||||
fun editNode(item: NetworkNodeRvItem)
|
||||
|
||||
fun addNewNode()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return when (viewType) {
|
||||
R.layout.item_chan_network_management_node -> ChainNetworkManagementNodeViewHolder(
|
||||
ItemChanNetworkManagementNodeBinding.inflate(parent.inflater(), parent, false),
|
||||
itemHandler
|
||||
)
|
||||
|
||||
R.layout.item_chan_network_management_add_node_button -> ChainNetworkManagementAddNodeButtonViewHolder(
|
||||
ItemChanNetworkManagementAddNodeButtonBinding.inflate(parent.inflater(), parent, false),
|
||||
itemHandler
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown view type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
if (holder is ChainNetworkManagementNodeViewHolder) {
|
||||
holder.bind(getItem(position) as NetworkNodeRvItem)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (getItem(position)) {
|
||||
is NetworkNodeRvItem -> R.layout.item_chan_network_management_node
|
||||
is NetworkNodesAddCustomRvItem -> R.layout.item_chan_network_management_add_node_button
|
||||
else -> throw IllegalArgumentException("Unknown item type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChainNetworkManagementAddNodeButtonViewHolder(
|
||||
private val binder: ItemChanNetworkManagementAddNodeButtonBinding,
|
||||
private val itemHandler: ChainNetworkManagementNodesAdapter.ItemHandler
|
||||
) : ViewHolder(binder.root) {
|
||||
|
||||
init {
|
||||
itemView.background = itemView.context.getMaskedRipple(cornerSizeInDp = 0)
|
||||
itemView.setOnClickListener {
|
||||
itemHandler.addNewNode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChainNetworkManagementNodeViewHolder(
|
||||
private val binder: ItemChanNetworkManagementNodeBinding,
|
||||
private val itemHandler: ChainNetworkManagementNodesAdapter.ItemHandler
|
||||
) : ViewHolder(binder.root) {
|
||||
|
||||
init {
|
||||
itemView.background = itemView.context.getMaskedRipple(cornerSizeInDp = 0)
|
||||
}
|
||||
|
||||
fun bind(item: NetworkNodeRvItem) {
|
||||
with(binder) {
|
||||
if (item.isSelectable) {
|
||||
itemView.setOnClickListener { itemHandler.selectNode(item) }
|
||||
} else {
|
||||
itemView.setOnClickListener(null)
|
||||
}
|
||||
|
||||
chainNodeRadioButton.isChecked = item.isSelected
|
||||
chainNodeRadioButton.isEnabled = item.isSelectable
|
||||
chainNodeName.text = item.name
|
||||
chainNodeName.setTextColorRes(item.nameColorRes)
|
||||
chainNodeSocketAddress.text = item.unformattedUrl
|
||||
chainNodeConnectionStatusShimmering.setShimmerShown(item.connectionState.showShimmering)
|
||||
chainNodeConnectionState.setText(item.connectionState.name)
|
||||
item.connectionState.chainStatusColor?.let { chainNodeConnectionState.setTextColor(it) }
|
||||
chainNodeConnectionState.setDrawableStart(item.connectionState.chainStatusIcon, paddingInDp = 6)
|
||||
chainNodeConnectionState.setCompoundDrawableTint(item.connectionState.chainStatusIconColor)
|
||||
|
||||
chainNodeEditButton.isVisible = item.isEditable && !item.isDeletable
|
||||
chainNodeManageButton.isVisible = item.isEditable && item.isDeletable
|
||||
chainNodeEditButton.setOnClickListener { itemHandler.editNode(item) }
|
||||
chainNodeManageButton.setOnClickListener { itemHandler.editNode(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<NetworkConnectionRvItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: NetworkConnectionRvItem, newItem: NetworkConnectionRvItem): Boolean {
|
||||
return when (oldItem) {
|
||||
is NetworkNodeRvItem -> newItem is NetworkNodeRvItem && oldItem.id == newItem.id
|
||||
is NetworkNodesAddCustomRvItem -> newItem is NetworkNodesAddCustomRvItem
|
||||
else -> throw IllegalArgumentException("Unknown item type")
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DiffUtilEquals")
|
||||
override fun areContentsTheSame(oldItem: NetworkConnectionRvItem, newItem: NetworkConnectionRvItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.novafoundation.nova.common.list.decoration.BackgroundItemDecoration
|
||||
import io.novafoundation.nova.common.list.decoration.DividerItemDecoration
|
||||
|
||||
class NodesItemBackgroundDecoration(context: Context) : BackgroundItemDecoration(
|
||||
context = context,
|
||||
outerHorizontalMarginDp = 16,
|
||||
innerVerticalPaddingDp = 0
|
||||
) {
|
||||
|
||||
override fun shouldApplyDecoration(holder: RecyclerView.ViewHolder): Boolean {
|
||||
return holder.isNodeItem()
|
||||
}
|
||||
}
|
||||
|
||||
class NodesItemDividerDecoration(context: Context) : DividerItemDecoration(context, dividerMarginDp = 32) {
|
||||
|
||||
override fun shouldApplyDecorationBetween(top: RecyclerView.ViewHolder, bottom: RecyclerView.ViewHolder): Boolean {
|
||||
return top.isNodeItem() && bottom.isNodeItem()
|
||||
}
|
||||
}
|
||||
|
||||
private fun RecyclerView.ViewHolder.isNodeItem(): Boolean {
|
||||
return this is ChainNetworkManagementNodeViewHolder || this is ChainNetworkManagementAddNodeButtonViewHolder
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.items
|
||||
|
||||
interface NetworkConnectionRvItem
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.items
|
||||
|
||||
import androidx.annotation.ColorRes
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.common.ConnectionStateModel
|
||||
|
||||
data class NetworkNodeRvItem(
|
||||
val id: String,
|
||||
val name: String,
|
||||
@ColorRes val nameColorRes: Int,
|
||||
val unformattedUrl: String,
|
||||
val isEditable: Boolean,
|
||||
val isDeletable: Boolean,
|
||||
val isSelected: Boolean,
|
||||
val connectionState: ConnectionStateModel,
|
||||
val isSelectable: Boolean
|
||||
) : NetworkConnectionRvItem
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.nodeAdapter.items
|
||||
|
||||
class NetworkNodesAddCustomRvItem : NetworkConnectionRvItem
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.common
|
||||
|
||||
data class ConnectionStateModel(
|
||||
val name: String?,
|
||||
val chainStatusColor: Int?,
|
||||
val chainStatusIcon: Int,
|
||||
val chainStatusIconColor: Int?,
|
||||
val showShimmering: Boolean
|
||||
)
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.main
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.setupWithViewPager2
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.FragmentNetworkManagementBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent
|
||||
|
||||
class NetworkManagementListFragment : BaseFragment<NetworkManagementListViewModel, FragmentNetworkManagementBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_OPEN_ADDED_TAB = "key_payload"
|
||||
|
||||
fun getBundle(openAddedTab: Boolean): Bundle {
|
||||
return Bundle().apply {
|
||||
putBoolean(KEY_OPEN_ADDED_TAB, openAddedTab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentNetworkManagementBinding.inflate(layoutInflater)
|
||||
|
||||
override fun initViews() {
|
||||
binder.networkManagementToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
binder.networkManagementToolbar.setRightActionClickListener { viewModel.addNetworkClicked() }
|
||||
|
||||
val adapter = NetworkManagementPagerAdapter(this)
|
||||
binder.networkManagementViewPager.adapter = adapter
|
||||
binder.networkManagementTabLayout.setupWithViewPager2(binder.networkManagementViewPager, adapter::getPageTitle)
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
setDefaultTab(adapter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDefaultTab(adapter: NetworkManagementPagerAdapter) {
|
||||
val openAddedTab = argumentOrNull<Boolean>(KEY_OPEN_ADDED_TAB) ?: return
|
||||
val tabIndex = if (openAddedTab) adapter.addedTabIndex() else adapter.defaultTabIndex()
|
||||
binder.networkManagementViewPager.currentItem = tabIndex
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<SettingsFeatureComponent>(
|
||||
requireContext(),
|
||||
SettingsFeatureApi::class.java
|
||||
)
|
||||
.networkManagementListFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: NetworkManagementListViewModel) {
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.main
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
|
||||
class NetworkManagementListViewModel(
|
||||
private val router: SettingsRouter
|
||||
) : BaseViewModel() {
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun addNetworkClicked() {
|
||||
router.addNetwork()
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.main
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.addedNetworks.AddedNetworkListFragment
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.defaultNetworks.ExistingNetworkListFragment
|
||||
|
||||
class NetworkManagementPagerAdapter(private val fragment: Fragment) : FragmentStateAdapter(fragment) {
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> ExistingNetworkListFragment()
|
||||
1 -> AddedNetworkListFragment()
|
||||
else -> throw IllegalArgumentException("Invalid position")
|
||||
}
|
||||
}
|
||||
|
||||
fun getPageTitle(position: Int): CharSequence {
|
||||
return when (position) {
|
||||
defaultTabIndex() -> fragment.getString(R.string.network_management_default_page_title)
|
||||
addedTabIndex() -> fragment.getString(R.string.network_management_added_page_title)
|
||||
else -> throw IllegalArgumentException("Invalid position")
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultTabIndex(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
fun addedTabIndex(): Int {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.main.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.main.NetworkManagementListFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
NetworkManagementListModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface NetworkManagementListComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
): NetworkManagementListComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: NetworkManagementListFragment)
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.main.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.main.NetworkManagementListViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class NetworkManagementListModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(NetworkManagementListViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: SettingsRouter
|
||||
): ViewModel {
|
||||
return NetworkManagementListViewModel(
|
||||
router
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): NetworkManagementListViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(NetworkManagementListViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.addedNetworks
|
||||
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.list.EditablePlaceholderAdapter
|
||||
import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents
|
||||
import io.novafoundation.nova.common.utils.ViewSpace
|
||||
import io.novafoundation.nova.common.view.PlaceholderModel
|
||||
import io.novafoundation.nova.common.view.PlaceholderView
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.addedNetworks.adapter.NetworksBannerAdapter
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.NetworkListFragment
|
||||
|
||||
class AddedNetworkListFragment : NetworkListFragment<AddedNetworkListViewModel>(), NetworksBannerAdapter.ItemHandler {
|
||||
|
||||
private val bannerAdapter = NetworksBannerAdapter(this)
|
||||
|
||||
private val placeholderAdapter = EditablePlaceholderAdapter()
|
||||
|
||||
override val adapter: RecyclerView.Adapter<*> by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ConcatAdapter(bannerAdapter, placeholderAdapter, networksAdapter)
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<SettingsFeatureComponent>(
|
||||
requireContext(),
|
||||
SettingsFeatureApi::class.java
|
||||
)
|
||||
.addedNetworkListFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
super.initViews()
|
||||
placeholderAdapter.setPadding(ViewSpace(top = 64.dp))
|
||||
placeholderAdapter.setPlaceholderData(
|
||||
PlaceholderModel(
|
||||
requireContext().getString(R.string.added_networks_empty_placeholder),
|
||||
R.drawable.ic_no_added_networks,
|
||||
requireContext().getString(R.string.common_add_network),
|
||||
PlaceholderView.Style.NO_BACKGROUND,
|
||||
imageTint = null
|
||||
)
|
||||
)
|
||||
placeholderAdapter.setButtonClickListener { viewModel.addNetworkClicked() }
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: AddedNetworkListViewModel) {
|
||||
super.subscribe(viewModel)
|
||||
|
||||
observeBrowserEvents(viewModel)
|
||||
|
||||
viewModel.showBanner.observe { bannerAdapter.show(it) }
|
||||
viewModel.networkList.observe { placeholderAdapter.show(it.isEmpty()) }
|
||||
}
|
||||
|
||||
override fun closeBannerClicked() {
|
||||
viewModel.closeBannerClicked()
|
||||
}
|
||||
|
||||
override fun bannerWikiLinkClicked() {
|
||||
viewModel.bannerWikiClicked()
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.addedNetworks
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.data.network.AppLinksProvider
|
||||
import io.novafoundation.nova.common.mixin.api.Browserable
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.event
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.NetworkManagementInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.NetworkListAdapterItemFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.NetworkListViewModel
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.adapter.NetworkListRvItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AddedNetworkListViewModel(
|
||||
private val networkManagementInteractor: NetworkManagementInteractor,
|
||||
private val networkListAdapterItemFactory: NetworkListAdapterItemFactory,
|
||||
private val appLinksProvider: AppLinksProvider,
|
||||
private val router: SettingsRouter
|
||||
) : NetworkListViewModel(router), Browserable {
|
||||
|
||||
private val networks = networkManagementInteractor.addedNetworksFlow()
|
||||
.shareInBackground()
|
||||
|
||||
override val networkList: Flow<List<NetworkListRvItem>> = networks.mapList {
|
||||
networkListAdapterItemFactory.getNetworkItem(it)
|
||||
}
|
||||
|
||||
override val openBrowserEvent = MutableLiveData<Event<String>>()
|
||||
|
||||
val showBanner = networkManagementInteractor.shouldShowBanner()
|
||||
.shareInBackground()
|
||||
|
||||
fun closeBannerClicked() {
|
||||
launch {
|
||||
networkManagementInteractor.hideBanner()
|
||||
}
|
||||
}
|
||||
|
||||
fun bannerWikiClicked() {
|
||||
openBrowserEvent.value = appLinksProvider.integrateNetwork.event()
|
||||
}
|
||||
|
||||
fun addNetworkClicked() {
|
||||
router.addNetwork()
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.addedNetworks.adapter
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.novafoundation.nova.common.list.SingleItemAdapter
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.ItemIntegrateNetworksBannerBinding
|
||||
|
||||
class NetworksBannerAdapter(
|
||||
private val itemHandler: ItemHandler
|
||||
) : SingleItemAdapter<NetworkBannerViewHolder>() {
|
||||
|
||||
interface ItemHandler {
|
||||
|
||||
fun closeBannerClicked()
|
||||
|
||||
fun bannerWikiLinkClicked()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NetworkBannerViewHolder {
|
||||
return NetworkBannerViewHolder(ItemIntegrateNetworksBannerBinding.inflate(parent.inflater(), parent, false), itemHandler)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: NetworkBannerViewHolder, position: Int) {
|
||||
// Not need to bind anything
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkBannerViewHolder(
|
||||
private val binder: ItemIntegrateNetworksBannerBinding,
|
||||
private val itemHandler: NetworksBannerAdapter.ItemHandler
|
||||
) : RecyclerView.ViewHolder(binder.root) {
|
||||
|
||||
init {
|
||||
with(binder) {
|
||||
integrateNetworkBannerClose.setOnClickListener { itemHandler.closeBannerClicked() }
|
||||
integrateNetworkBannerLink.setOnClickListener { itemHandler.bannerWikiLinkClicked() }
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.addedNetworks.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.addedNetworks.AddedNetworkListFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
AddedNetworkListModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface AddedNetworkListComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
): AddedNetworkListComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: AddedNetworkListFragment)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.addedNetworks.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.data.network.AppLinksProvider
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.NetworkManagementInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.NetworkListAdapterItemFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.addedNetworks.AddedNetworkListViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class AddedNetworkListModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(AddedNetworkListViewModel::class)
|
||||
fun provideViewModel(
|
||||
networkManagementInteractor: NetworkManagementInteractor,
|
||||
networkListAdapterItemFactory: NetworkListAdapterItemFactory,
|
||||
appLinksProvider: AppLinksProvider,
|
||||
router: SettingsRouter
|
||||
): ViewModel {
|
||||
return AddedNetworkListViewModel(
|
||||
networkManagementInteractor,
|
||||
networkListAdapterItemFactory,
|
||||
appLinksProvider,
|
||||
router
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): AddedNetworkListViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(AddedNetworkListViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common
|
||||
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.asIconOrFallback
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.iconOrFallback
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.NetworkState
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.common.ConnectionStateModel
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.adapter.NetworkListRvItem
|
||||
import io.novafoundation.nova.runtime.ext.isDisabled
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.LightChain
|
||||
import io.novasama.substrate_sdk_android.wsrpc.state.SocketStateMachine
|
||||
|
||||
interface NetworkListAdapterItemFactory {
|
||||
|
||||
fun getNetworkItem(network: NetworkState): NetworkListRvItem
|
||||
|
||||
fun getNetworkItem(network: LightChain): NetworkListRvItem
|
||||
}
|
||||
|
||||
class RealNetworkListAdapterItemFactory(
|
||||
private val resourceManager: ResourceManager
|
||||
) : NetworkListAdapterItemFactory {
|
||||
|
||||
override fun getNetworkItem(network: NetworkState): NetworkListRvItem {
|
||||
val chain = network.chain
|
||||
val subtitle = if (chain.isDisabled) resourceManager.getString(R.string.common_disabled) else null
|
||||
val label = getChainLabel(chain)
|
||||
return NetworkListRvItem(
|
||||
chainIcon = chain.iconOrFallback(),
|
||||
chainId = chain.id,
|
||||
title = chain.name,
|
||||
subtitle = subtitle,
|
||||
chainLabel = label,
|
||||
disabled = chain.isDisabled,
|
||||
status = getConnectingState(network)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getNetworkItem(network: LightChain): NetworkListRvItem {
|
||||
return NetworkListRvItem(
|
||||
chainIcon = network.icon.asIconOrFallback(),
|
||||
chainId = network.id,
|
||||
title = network.name,
|
||||
subtitle = null,
|
||||
chainLabel = null,
|
||||
disabled = false,
|
||||
status = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun getChainLabel(chain: Chain): String? {
|
||||
return if (chain.isTestNet) {
|
||||
resourceManager.getString(R.string.common_testnet)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConnectingState(network: NetworkState): ConnectionStateModel? {
|
||||
if (network.chain.isDisabled) return null
|
||||
|
||||
return when (network.connectionState) {
|
||||
is SocketStateMachine.State.Connected -> null
|
||||
|
||||
else -> ConnectionStateModel(
|
||||
name = resourceManager.getString(R.string.common_connecting),
|
||||
chainStatusColor = resourceManager.getColor(R.color.text_primary),
|
||||
chainStatusIcon = R.drawable.ic_connection_status_connecting,
|
||||
chainStatusIconColor = resourceManager.getColor(R.color.icon_primary),
|
||||
showShimmering = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.FragmentNetworkListBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.adapter.NetworkManagementListAdapter
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class NetworkListFragment<T : NetworkListViewModel> : BaseFragment<T, FragmentNetworkListBinding>(), NetworkManagementListAdapter.ItemHandler {
|
||||
|
||||
override fun createBinding() = FragmentNetworkListBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
protected val networksAdapter by lazy(LazyThreadSafetyMode.NONE) { NetworkManagementListAdapter(imageLoader, this) }
|
||||
|
||||
protected abstract val adapter: RecyclerView.Adapter<*>
|
||||
|
||||
override fun initViews() {
|
||||
binder.networkList.adapter = adapter
|
||||
binder.networkList.itemAnimator = null
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: T) {
|
||||
viewModel.networkList.observe { networksAdapter.submitList(it) }
|
||||
}
|
||||
|
||||
override fun onNetworkClicked(chainId: String) {
|
||||
viewModel.onNetworkClicked(chainId)
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.chain.ChainNetworkManagementPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.adapter.NetworkListRvItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
abstract class NetworkListViewModel(
|
||||
private val router: SettingsRouter
|
||||
) : BaseViewModel() {
|
||||
|
||||
abstract val networkList: Flow<List<NetworkListRvItem>>
|
||||
|
||||
fun onNetworkClicked(chainId: String) {
|
||||
router.openNetworkDetails(ChainNetworkManagementPayload(chainId))
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.adapter
|
||||
|
||||
import io.novafoundation.nova.common.utils.images.Icon
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.common.ConnectionStateModel
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
|
||||
data class NetworkListRvItem(
|
||||
val chainIcon: Icon,
|
||||
val chainId: ChainId,
|
||||
val title: String,
|
||||
val subtitle: String?,
|
||||
val chainLabel: String?,
|
||||
val disabled: Boolean,
|
||||
val status: ConnectionStateModel?
|
||||
)
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.utils.images.setIcon
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setCompoundDrawableTint
|
||||
import io.novafoundation.nova.common.utils.setDrawableStart
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.common.utils.setTextOrHide
|
||||
import io.novafoundation.nova.common.view.shape.getMaskedRipple
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.ItemNetworkSettingsBinding
|
||||
|
||||
class NetworkManagementListAdapter(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val itemHandler: ItemHandler
|
||||
) : ListAdapter<NetworkListRvItem, NetworkListViewHolder>(NetworkManagementListDiffCallback()) {
|
||||
|
||||
interface ItemHandler {
|
||||
|
||||
fun onNetworkClicked(chainId: String)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NetworkListViewHolder {
|
||||
return NetworkListViewHolder(ItemNetworkSettingsBinding.inflate(parent.inflater(), parent, false), imageLoader, itemHandler)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: NetworkListViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkManagementListDiffCallback : DiffUtil.ItemCallback<NetworkListRvItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: NetworkListRvItem, newItem: NetworkListRvItem): Boolean {
|
||||
return oldItem.chainId == newItem.chainId
|
||||
}
|
||||
|
||||
@SuppressLint("DiffUtilEquals")
|
||||
override fun areContentsTheSame(oldItem: NetworkListRvItem, newItem: NetworkListRvItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkListViewHolder(
|
||||
private val binder: ItemNetworkSettingsBinding,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val itemHandler: NetworkManagementListAdapter.ItemHandler
|
||||
) : ViewHolder(binder.root) {
|
||||
|
||||
init {
|
||||
itemView.background = itemView.context.getMaskedRipple(cornerSizeInDp = 0)
|
||||
}
|
||||
|
||||
fun bind(item: NetworkListRvItem) = with(binder) {
|
||||
itemView.setOnClickListener { itemHandler.onNetworkClicked(item.chainId) }
|
||||
|
||||
itemNetworkImage.setIcon(item.chainIcon, imageLoader)
|
||||
itemNetworkTitle.text = item.title
|
||||
itemNetworkSubtitle.setTextOrHide(item.subtitle)
|
||||
itemNetworkLabel.setTextOrHide(item.chainLabel)
|
||||
|
||||
itemNetworkStatusShimmer.isVisible = item.status != null
|
||||
if (item.status != null) {
|
||||
itemNetworkStatus.setText(item.status.name)
|
||||
item.status.chainStatusColor?.let { itemNetworkStatus.setTextColor(it) }
|
||||
itemNetworkStatus.setDrawableStart(item.status.chainStatusIcon, paddingInDp = 6)
|
||||
itemNetworkStatus.setCompoundDrawableTint(item.status.chainStatusIconColor)
|
||||
}
|
||||
|
||||
if (item.disabled) {
|
||||
itemNetworkImage.alpha = 0.32f
|
||||
itemNetworkTitle.setTextColorRes(R.color.text_secondary)
|
||||
} else {
|
||||
itemNetworkImage.alpha = 1f
|
||||
itemNetworkTitle.setTextColorRes(R.color.text_primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.defaultNetworks
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.NetworkListFragment
|
||||
|
||||
class ExistingNetworkListFragment : NetworkListFragment<ExistingNetworkListViewModel>() {
|
||||
|
||||
override val adapter: RecyclerView.Adapter<*> by lazy(LazyThreadSafetyMode.NONE) { networksAdapter }
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<SettingsFeatureComponent>(
|
||||
requireContext(),
|
||||
SettingsFeatureApi::class.java
|
||||
)
|
||||
.existingNetworkListFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.defaultNetworks
|
||||
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.NetworkManagementInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.NetworkListAdapterItemFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.NetworkListViewModel
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.adapter.NetworkListRvItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class ExistingNetworkListViewModel(
|
||||
private val networkManagementInteractor: NetworkManagementInteractor,
|
||||
private val networkListAdapterItemFactory: NetworkListAdapterItemFactory,
|
||||
router: SettingsRouter
|
||||
) : NetworkListViewModel(router) {
|
||||
|
||||
private val networks = networkManagementInteractor.defaultNetworksFlow()
|
||||
.shareInBackground()
|
||||
|
||||
override val networkList: Flow<List<NetworkListRvItem>> = networks.mapList {
|
||||
networkListAdapterItemFactory.getNetworkItem(it)
|
||||
}.shareInBackground()
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.defaultNetworks.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.defaultNetworks.ExistingNetworkListFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
ExistingNetworkListModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface ExistingNetworkListComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
): ExistingNetworkListComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: ExistingNetworkListFragment)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.defaultNetworks.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.NetworkManagementInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.NetworkListAdapterItemFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.defaultNetworks.ExistingNetworkListViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class ExistingNetworkListModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(ExistingNetworkListViewModel::class)
|
||||
fun provideViewModel(
|
||||
networkManagementInteractor: NetworkManagementInteractor,
|
||||
networkListAdapterItemFactory: NetworkListAdapterItemFactory,
|
||||
router: SettingsRouter
|
||||
): ViewModel {
|
||||
return ExistingNetworkListViewModel(
|
||||
networkManagementInteractor,
|
||||
networkListAdapterItemFactory,
|
||||
router
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): ExistingNetworkListViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(ExistingNetworkListViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.preConfiguredNetworks
|
||||
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.domain.ExtendedLoadingState
|
||||
import io.novafoundation.nova.common.domain.isLoading
|
||||
import io.novafoundation.nova.common.mixin.impl.observeRetries
|
||||
import io.novafoundation.nova.common.utils.bindTo
|
||||
import io.novafoundation.nova.common.utils.progress.observeProgressDialog
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.FragmentPreConfiguredNetworkListBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.adapter.NetworkManagementListAdapter
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.preConfiguredNetworks.adapter.AddCustomNetworkAdapter
|
||||
import javax.inject.Inject
|
||||
|
||||
class PreConfiguredNetworksFragment :
|
||||
BaseFragment<PreConfiguredNetworksViewModel, FragmentPreConfiguredNetworkListBinding>(),
|
||||
NetworkManagementListAdapter.ItemHandler,
|
||||
AddCustomNetworkAdapter.ItemHandler {
|
||||
|
||||
override fun createBinding() = FragmentPreConfiguredNetworkListBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val addCustomNetworkAdapter = AddCustomNetworkAdapter(this)
|
||||
|
||||
private val networksAdapter by lazy(LazyThreadSafetyMode.NONE) { NetworkManagementListAdapter(imageLoader, this) }
|
||||
|
||||
private val adapter by lazy(LazyThreadSafetyMode.NONE) { ConcatAdapter(addCustomNetworkAdapter, networksAdapter) }
|
||||
|
||||
override fun initViews() {
|
||||
binder.preConfiguredNetworksToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
binder.preConfiguredNetworkList.adapter = adapter
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<SettingsFeatureComponent>(
|
||||
requireContext(),
|
||||
SettingsFeatureApi::class.java
|
||||
)
|
||||
.preConfiguredNetworks()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: PreConfiguredNetworksViewModel) {
|
||||
observeRetries(viewModel)
|
||||
observeProgressDialog(viewModel.progressDialogMixin)
|
||||
|
||||
binder.preConfiguredNetworksSearch.content.bindTo(viewModel.searchQuery, viewModel.viewModelScope)
|
||||
viewModel.networkList.observe {
|
||||
binder.preConfiguredNetworkProgress.isVisible = it.isLoading()
|
||||
if (it is ExtendedLoadingState.Loaded) {
|
||||
networksAdapter.submitList(it.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNetworkClicked(chainId: String) {
|
||||
viewModel.networkClicked(chainId)
|
||||
}
|
||||
|
||||
override fun onAddNetworkClicked() {
|
||||
viewModel.addNetworkClicked()
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.preConfiguredNetworks
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.domain.ExtendedLoadingState
|
||||
import io.novafoundation.nova.common.domain.map
|
||||
import io.novafoundation.nova.common.domain.mapLoading
|
||||
import io.novafoundation.nova.common.mixin.api.Retriable
|
||||
import io.novafoundation.nova.common.mixin.api.RetryPayload
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.progress.ProgressDialogMixinFactory
|
||||
import io.novafoundation.nova.common.utils.progress.startProgress
|
||||
import io.novafoundation.nova.feature_push_notifications.R
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.PreConfiguredNetworksInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.add.main.AddNetworkPayload
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.NetworkListAdapterItemFactory
|
||||
import io.novafoundation.nova.runtime.ext.networkType
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.LightChain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.NetworkType
|
||||
import io.novafoundation.nova.runtime.util.ChainParcel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
typealias LoadingNetworks = ExtendedLoadingState<List<LightChain>>
|
||||
|
||||
class PreConfiguredNetworksViewModel(
|
||||
private val interactor: PreConfiguredNetworksInteractor,
|
||||
private val networkListAdapterItemFactory: NetworkListAdapterItemFactory,
|
||||
private val router: SettingsRouter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val progressDialogMixinFactory: ProgressDialogMixinFactory
|
||||
) : BaseViewModel(), Retriable {
|
||||
|
||||
val progressDialogMixin = progressDialogMixinFactory.create()
|
||||
|
||||
val searchQuery: MutableStateFlow<String> = MutableStateFlow("")
|
||||
private val allPreConfiguredNetworksFlow = MutableStateFlow<LoadingNetworks>(ExtendedLoadingState.Loading)
|
||||
|
||||
private val networks = combine(
|
||||
allPreConfiguredNetworksFlow,
|
||||
searchQuery,
|
||||
chainRegistry.chainsById
|
||||
) { preConfiguredNetworks, query, currentChains ->
|
||||
preConfiguredNetworks.map {
|
||||
val filteredNetworks = interactor.excludeChains(it, currentChains.keys)
|
||||
interactor.searchNetworks(query, filteredNetworks)
|
||||
}
|
||||
}
|
||||
|
||||
val networkList = networks.mapLoading { networks ->
|
||||
networks.map { networkListAdapterItemFactory.getNetworkItem(it) }
|
||||
}
|
||||
|
||||
override val retryEvent: MutableLiveData<Event<RetryPayload>> = MutableLiveData()
|
||||
|
||||
init {
|
||||
fetchPreConfiguredNetworks()
|
||||
}
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun networkClicked(chainId: String) {
|
||||
launch {
|
||||
progressDialogMixin.startProgress(R.string.loading_network_info) {
|
||||
interactor.getPreConfiguredNetwork(chainId)
|
||||
.onSuccess {
|
||||
openAddChainScreen(it)
|
||||
}
|
||||
.onFailure { showError(resourceManager.getString(R.string.common_something_went_wrong_title)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openAddChainScreen(chain: Chain) {
|
||||
val networkType = when (chain.networkType()) {
|
||||
NetworkType.SUBSTRATE -> AddNetworkPayload.Mode.Add.NetworkType.SUBSTRATE
|
||||
NetworkType.EVM -> AddNetworkPayload.Mode.Add.NetworkType.EVM
|
||||
}
|
||||
|
||||
val payload = AddNetworkPayload.Mode.Add(
|
||||
networkType = networkType,
|
||||
ChainParcel(chain)
|
||||
)
|
||||
|
||||
router.openCreateNetworkFlow(payload)
|
||||
}
|
||||
|
||||
fun addNetworkClicked() {
|
||||
router.openCreateNetworkFlow()
|
||||
}
|
||||
|
||||
private fun fetchPreConfiguredNetworks() {
|
||||
launch {
|
||||
allPreConfiguredNetworksFlow.value = ExtendedLoadingState.Loading
|
||||
|
||||
interactor.getPreConfiguredNetworks()
|
||||
.onSuccess {
|
||||
allPreConfiguredNetworksFlow.value = ExtendedLoadingState.Loaded(it)
|
||||
}.onFailure {
|
||||
launchRetryDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchRetryDialog() {
|
||||
retryEvent.value = Event(
|
||||
RetryPayload(
|
||||
title = resourceManager.getString(R.string.common_error_general_title),
|
||||
message = resourceManager.getString(R.string.common_retry_message),
|
||||
onRetry = { fetchPreConfiguredNetworks() },
|
||||
onCancel = { router.back() }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.preConfiguredNetworks.adapter
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import io.novafoundation.nova.common.utils.inflateChild
|
||||
import io.novafoundation.nova.common.view.shape.getMaskedRipple
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
|
||||
class AddCustomNetworkAdapter(
|
||||
private val itemHandler: ItemHandler
|
||||
) : RecyclerView.Adapter<AddCustomNetworkViewHolder>() {
|
||||
|
||||
interface ItemHandler {
|
||||
|
||||
fun onAddNetworkClicked()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddCustomNetworkViewHolder {
|
||||
return AddCustomNetworkViewHolder(parent, itemHandler)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: AddCustomNetworkViewHolder, position: Int) {}
|
||||
}
|
||||
|
||||
class AddCustomNetworkViewHolder(
|
||||
parent: ViewGroup,
|
||||
private val itemHandler: AddCustomNetworkAdapter.ItemHandler
|
||||
) : ViewHolder(parent.inflateChild(R.layout.item_add_custom_network)) {
|
||||
|
||||
init {
|
||||
itemView.background = itemView.context.getMaskedRipple(cornerSizeInDp = 0)
|
||||
itemView.setOnClickListener { itemHandler.onAddNetworkClicked() }
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.preConfiguredNetworks.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.preConfiguredNetworks.PreConfiguredNetworksFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
PreConfiguredNetworksModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface PreConfiguredNetworksComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
): PreConfiguredNetworksComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: PreConfiguredNetworksFragment)
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.preConfiguredNetworks.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.data.network.coingecko.CoinGeckoLinkParser
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.progress.ProgressDialogMixinFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.PreConfiguredNetworksInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.common.NetworkListAdapterItemFactory
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.networkList.preConfiguredNetworks.PreConfiguredNetworksViewModel
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class PreConfiguredNetworksModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(PreConfiguredNetworksViewModel::class)
|
||||
fun provideViewModel(
|
||||
preConfiguredNetworksInteractor: PreConfiguredNetworksInteractor,
|
||||
networkListAdapterItemFactory: NetworkListAdapterItemFactory,
|
||||
router: SettingsRouter,
|
||||
resourceManager: ResourceManager,
|
||||
chainRegistry: ChainRegistry,
|
||||
progressDialogMixinFactory: ProgressDialogMixinFactory,
|
||||
coinGeckoLinkParser: CoinGeckoLinkParser
|
||||
): ViewModel {
|
||||
return PreConfiguredNetworksViewModel(
|
||||
interactor = preConfiguredNetworksInteractor,
|
||||
networkListAdapterItemFactory = networkListAdapterItemFactory,
|
||||
router = router,
|
||||
resourceManager = resourceManager,
|
||||
chainRegistry = chainRegistry,
|
||||
progressDialogMixinFactory = progressDialogMixinFactory
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): PreConfiguredNetworksViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(PreConfiguredNetworksViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.mixin.impl.observeValidations
|
||||
import io.novafoundation.nova.common.utils.bindTo
|
||||
import io.novafoundation.nova.common.view.setState
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.FragmentCustomNodeBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent
|
||||
import javax.inject.Inject
|
||||
|
||||
class CustomNodeFragment : BaseFragment<CustomNodeViewModel, FragmentCustomNodeBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_PAYLOAD = "payload"
|
||||
|
||||
fun getBundle(payload: CustomNodePayload): Bundle {
|
||||
return Bundle().apply {
|
||||
putParcelable(EXTRA_PAYLOAD, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentCustomNodeBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
override fun initViews() {
|
||||
binder.customNodeToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
|
||||
binder.customNodeApplyButton.prepareForProgress(this)
|
||||
binder.customNodeApplyButton.setOnClickListener { viewModel.saveNodeClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<SettingsFeatureComponent>(
|
||||
requireContext(),
|
||||
SettingsFeatureApi::class.java
|
||||
)
|
||||
.customNodeFactory()
|
||||
.create(this, argument(EXTRA_PAYLOAD))
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: CustomNodeViewModel) {
|
||||
observeValidations(viewModel)
|
||||
|
||||
binder.customNodeTitle.text = viewModel.getTitle()
|
||||
binder.customNodeUrlInput.bindTo(viewModel.nodeUrlInput, viewModel)
|
||||
binder.customNodeNameInput.bindTo(viewModel.nodeNameInput, viewModel)
|
||||
viewModel.buttonState.observe {
|
||||
binder.customNodeApplyButton.setState(it)
|
||||
}
|
||||
|
||||
viewModel.chainModel.observe {
|
||||
binder.customNodeChain.setChain(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class CustomNodePayload(
|
||||
val chainId: String,
|
||||
val mode: Mode
|
||||
) : Parcelable {
|
||||
|
||||
sealed interface Mode : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
object Add : Mode
|
||||
|
||||
@Parcelize
|
||||
class Edit(val url: String) : Mode
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.mixin.api.Validatable
|
||||
import io.novafoundation.nova.common.presentation.DescriptiveButtonState
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.common.validation.progressConsumer
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.CustomNodeInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNode.NetworkNodePayload
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CustomNodeViewModel(
|
||||
private val router: SettingsRouter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val payload: CustomNodePayload,
|
||||
private val customNodeInteractor: CustomNodeInteractor,
|
||||
private val validationExecutor: ValidationExecutor,
|
||||
private val chainRegistry: ChainRegistry
|
||||
) : BaseViewModel(), Validatable by validationExecutor {
|
||||
|
||||
val chainModel = flowOf { chainRegistry.getChain(payload.chainId) }
|
||||
.map { mapChainToUi(it) }
|
||||
|
||||
val nodeUrlInput = MutableStateFlow("")
|
||||
val nodeNameInput = MutableStateFlow("")
|
||||
|
||||
private val saveProgressFlow = MutableStateFlow(false)
|
||||
|
||||
val buttonState = combine(saveProgressFlow, nodeUrlInput, nodeNameInput) { validationProgress, url, name ->
|
||||
when {
|
||||
validationProgress -> DescriptiveButtonState.Loading
|
||||
|
||||
url.isBlank() || name.isBlank() -> {
|
||||
DescriptiveButtonState.Disabled(resourceManager.getString(R.string.custom_node_disabled_button_state))
|
||||
}
|
||||
|
||||
else -> getEnabledButtonState()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
launch {
|
||||
if (payload.mode is CustomNodePayload.Mode.Edit) {
|
||||
customNodeInteractor.getNodeDetails(payload.chainId, payload.mode.url)
|
||||
.onSuccess { node ->
|
||||
nodeUrlInput.value = node.unformattedUrl
|
||||
nodeNameInput.value = node.name
|
||||
}.onFailure {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun getTitle(): String {
|
||||
return when (payload.mode) {
|
||||
CustomNodePayload.Mode.Add -> resourceManager.getString(R.string.custom_node_add_title)
|
||||
|
||||
is CustomNodePayload.Mode.Edit -> resourceManager.getString(R.string.custom_node_edit_title)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveNodeClicked() {
|
||||
launch {
|
||||
saveProgressFlow.value = true
|
||||
val validationPayload = NetworkNodePayload(
|
||||
chain = chainRegistry.getChain(payload.chainId),
|
||||
nodeUrl = nodeUrlInput.value
|
||||
)
|
||||
|
||||
validationExecutor.requireValid(
|
||||
validationSystem = customNodeInteractor.getValidationSystem(
|
||||
viewModelScope,
|
||||
skipNodeExistValidation = payload.mode is CustomNodePayload.Mode.Edit
|
||||
),
|
||||
payload = validationPayload,
|
||||
progressConsumer = saveProgressFlow.progressConsumer(),
|
||||
validationFailureTransformerCustom = { status, actions -> mapSaveCustomNodeFailureToUI(resourceManager, status) }
|
||||
) {
|
||||
saveProgressFlow.value = false
|
||||
|
||||
executeSaving()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeSaving() {
|
||||
launch {
|
||||
when (payload.mode) {
|
||||
CustomNodePayload.Mode.Add -> customNodeInteractor.createNode(payload.chainId, nodeUrlInput.value, nodeNameInput.value)
|
||||
|
||||
is CustomNodePayload.Mode.Edit -> customNodeInteractor.updateNode(payload.chainId, payload.mode.url, nodeUrlInput.value, nodeNameInput.value)
|
||||
}
|
||||
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEnabledButtonState(): DescriptiveButtonState.Enabled {
|
||||
return when (payload.mode) {
|
||||
CustomNodePayload.Mode.Add -> DescriptiveButtonState.Enabled(resourceManager.getString(R.string.custom_node_add_button_state))
|
||||
|
||||
is CustomNodePayload.Mode.Edit -> DescriptiveButtonState.Enabled(resourceManager.getString(R.string.custom_node_edit_button_state))
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node
|
||||
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.validation.TransformedFailure
|
||||
import io.novafoundation.nova.common.validation.TransformedFailure.Default
|
||||
import io.novafoundation.nova.common.validation.ValidationStatus
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.validation.customNode.NetworkNodeFailure
|
||||
|
||||
fun mapSaveCustomNodeFailureToUI(
|
||||
resourceManager: ResourceManager,
|
||||
status: ValidationStatus.NotValid<NetworkNodeFailure>
|
||||
): TransformedFailure {
|
||||
return when (val reason = status.reason) {
|
||||
is NetworkNodeFailure.NodeAlreadyExists -> Default(
|
||||
resourceManager.getString(R.string.node_already_exist_failure_title) to
|
||||
resourceManager.getString(R.string.node_already_exist_failure_message, reason.node.name)
|
||||
)
|
||||
|
||||
NetworkNodeFailure.NodeIsNotAlive -> Default(
|
||||
resourceManager.getString(R.string.node_not_alive_title) to
|
||||
resourceManager.getString(R.string.node_not_alive_message)
|
||||
)
|
||||
|
||||
is NetworkNodeFailure.WrongNetwork -> Default(
|
||||
resourceManager.getString(R.string.node_not_supported_by_network_title) to
|
||||
resourceManager.getString(R.string.node_not_supported_by_network_message, reason.chain.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node.CustomNodeFragment
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node.CustomNodePayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
CustomNodeModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface CustomNodeComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: CustomNodePayload
|
||||
): CustomNodeComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: CustomNodeFragment)
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.domain.CustomNodeInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node.CustomNodePayload
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.networkManagement.node.CustomNodeViewModel
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class CustomNodeModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(CustomNodeViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: SettingsRouter,
|
||||
resourceManager: ResourceManager,
|
||||
payload: CustomNodePayload,
|
||||
customNodeInteractor: CustomNodeInteractor,
|
||||
validationExecutor: ValidationExecutor,
|
||||
chainRegistry: ChainRegistry
|
||||
): ViewModel {
|
||||
return CustomNodeViewModel(
|
||||
router,
|
||||
resourceManager,
|
||||
payload,
|
||||
customNodeInteractor,
|
||||
validationExecutor,
|
||||
chainRegistry
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): CustomNodeViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(CustomNodeViewModel::class.java)
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.setupConfirmationDialog
|
||||
import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents
|
||||
import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets
|
||||
import io.novafoundation.nova.common.utils.sendEmailIntent
|
||||
import io.novafoundation.nova.common.view.dialog.dialog
|
||||
import io.novafoundation.nova.feature_settings_api.SettingsFeatureApi
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.FragmentSettingsBinding
|
||||
import io.novafoundation.nova.feature_settings_impl.di.SettingsFeatureComponent
|
||||
|
||||
class SettingsFragment : BaseFragment<SettingsViewModel, FragmentSettingsBinding>() {
|
||||
|
||||
override fun createBinding() = FragmentSettingsBinding.inflate(layoutInflater)
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.settingsContainer.applyStatusBarInsets()
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.accountView.setWholeClickListener { viewModel.accountActionsClicked() }
|
||||
|
||||
binder.settingsWallets.setOnClickListener { viewModel.walletsClicked() }
|
||||
binder.settingsNetworks.setOnClickListener { viewModel.networksClicked() }
|
||||
binder.settingsPushNotifications.setOnClickListener { viewModel.pushNotificationsClicked() }
|
||||
binder.settingsCurrency.setOnClickListener { viewModel.currenciesClicked() }
|
||||
binder.settingsLanguage.setOnClickListener { viewModel.languagesClicked() }
|
||||
binder.settingsAppearance.setOnClickListener { viewModel.appearanceClicked() }
|
||||
|
||||
binder.settingsTelegram.setOnClickListener { viewModel.telegramClicked() }
|
||||
binder.settingsTwitter.setOnClickListener { viewModel.twitterClicked() }
|
||||
binder.settingsYoutube.setOnClickListener { viewModel.openYoutube() }
|
||||
|
||||
binder.settingsWebsite.setOnClickListener { viewModel.websiteClicked() }
|
||||
binder.settingsGithub.setOnClickListener { viewModel.githubClicked() }
|
||||
binder.settingsTerms.setOnClickListener { viewModel.termsClicked() }
|
||||
binder.settingsPrivacy.setOnClickListener { viewModel.privacyClicked() }
|
||||
|
||||
binder.settingsRateUs.setOnClickListener { viewModel.rateUsClicked() }
|
||||
binder.settingsWiki.setOnClickListener { viewModel.wikiClicked() }
|
||||
binder.settingsEmail.setOnClickListener { viewModel.emailClicked() }
|
||||
|
||||
binder.settingsBiometricAuth.setOnClickListener { viewModel.changeBiometricAuth() }
|
||||
binder.settingsPinCodeVerification.setOnClickListener { viewModel.changePincodeVerification() }
|
||||
binder.settingsSafeMode.setOnClickListener { viewModel.changeSafeMode() }
|
||||
binder.settingsHideBalances.setOnClickListener { viewModel.changeHideBalances() }
|
||||
binder.settingsPin.setOnClickListener { viewModel.changePinCodeClicked() }
|
||||
|
||||
binder.settingsCloudBackup.setOnClickListener { viewModel.cloudBackupClicked() }
|
||||
|
||||
binder.settingsWalletConnect.setOnClickListener { viewModel.walletConnectClicked() }
|
||||
|
||||
binder.settingsAvatar.setOnClickListener { viewModel.selectedWalletClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<SettingsFeatureComponent>(
|
||||
requireContext(),
|
||||
SettingsFeatureApi::class.java
|
||||
)
|
||||
.settingsComponentFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: SettingsViewModel) {
|
||||
setupConfirmationDialog(R.style.AccentAlertDialogTheme, viewModel.confirmationAwaitableAction)
|
||||
observeBrowserEvents(viewModel)
|
||||
|
||||
viewModel.selectedWalletModel.observe {
|
||||
binder.settingsAvatar.setModel(it)
|
||||
|
||||
binder.accountView.setAccountIcon(it.walletIcon)
|
||||
binder.accountView.setTitle(it.name)
|
||||
}
|
||||
|
||||
viewModel.pushNotificationsState.observe {
|
||||
binder.settingsPushNotifications.setValue(it)
|
||||
}
|
||||
|
||||
viewModel.selectedCurrencyFlow.observe {
|
||||
binder.settingsCurrency.setValue(it.code)
|
||||
}
|
||||
|
||||
viewModel.selectedLanguageFlow.observe {
|
||||
binder.settingsLanguage.setValue(it.displayName)
|
||||
}
|
||||
|
||||
viewModel.showBiometricNotReadyDialogEvent.observeEvent {
|
||||
showBiometricNotReadyDialog()
|
||||
}
|
||||
|
||||
viewModel.biometricAuthStatus.observe {
|
||||
binder.settingsBiometricAuth.setChecked(it)
|
||||
}
|
||||
|
||||
viewModel.biometricEventMessages.observe {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
viewModel.pinCodeVerificationStatus.observe {
|
||||
binder.settingsPinCodeVerification.setChecked(it)
|
||||
}
|
||||
|
||||
viewModel.safeModeStatus.observe {
|
||||
binder.settingsSafeMode.setChecked(it)
|
||||
}
|
||||
|
||||
viewModel.hideBalancesOnLaunchState.observe {
|
||||
binder.settingsHideBalances.setChecked(it)
|
||||
}
|
||||
|
||||
viewModel.appVersionFlow.observe(binder.settingsAppVersion::setText)
|
||||
|
||||
viewModel.openEmailEvent.observeEvent { requireContext().sendEmailIntent(it) }
|
||||
|
||||
viewModel.walletConnectSessionsUi.observe(binder.settingsWalletConnect::setValue)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.onResume()
|
||||
}
|
||||
|
||||
private fun showBiometricNotReadyDialog() {
|
||||
dialog(requireContext(), customStyle = R.style.AccentAlertDialogTheme) {
|
||||
setTitle(R.string.settings_biometric_not_ready_title)
|
||||
setMessage(R.string.settings_biometric_not_ready_message)
|
||||
setNegativeButton(R.string.common_cancel, null)
|
||||
setPositiveButton(getString(R.string.common_settings)) { _, _ ->
|
||||
startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
viewModel.onPause()
|
||||
}
|
||||
}
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.settings
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.data.network.AppLinksProvider
|
||||
import io.novafoundation.nova.common.domain.usecase.MaskingModeUseCase
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ConfirmationDialogInfo
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.fromRes
|
||||
import io.novafoundation.nova.common.mixin.api.Browserable
|
||||
import io.novafoundation.nova.common.resources.AppVersionProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.resources.formatBooleanToState
|
||||
import io.novafoundation.nova.common.sequrity.SafeModeService
|
||||
import io.novafoundation.nova.common.sequrity.TwoFactorVerificationResult
|
||||
import io.novafoundation.nova.common.sequrity.TwoFactorVerificationService
|
||||
import io.novafoundation.nova.common.sequrity.biometry.BiometricResponse
|
||||
import io.novafoundation.nova.common.sequrity.biometry.BiometricService
|
||||
import io.novafoundation.nova.common.sequrity.biometry.mapBiometricErrors
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.event
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.language.LanguageUseCase
|
||||
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
|
||||
import io.novafoundation.nova.feature_currency_api.presentation.mapper.mapCurrencyToUI
|
||||
import io.novafoundation.nova.feature_push_notifications.data.PushNotificationsAvailabilityState
|
||||
import io.novafoundation.nova.feature_push_notifications.domain.interactor.PushNotificationsInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.mapNumberOfActiveSessionsToUi
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SettingsViewModel(
|
||||
private val languageUseCase: LanguageUseCase,
|
||||
private val router: SettingsRouter,
|
||||
private val appLinksProvider: AppLinksProvider,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
private val currencyInteractor: CurrencyInteractor,
|
||||
private val safeModeService: SafeModeService,
|
||||
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
private val walletConnectSessionsUseCase: WalletConnectSessionsUseCase,
|
||||
private val twoFactorVerificationService: TwoFactorVerificationService,
|
||||
private val biometricService: BiometricService,
|
||||
private val pushNotificationsInteractor: PushNotificationsInteractor,
|
||||
private val maskingModeUseCase: MaskingModeUseCase
|
||||
) : BaseViewModel(), Browserable {
|
||||
|
||||
val confirmationAwaitableAction = actionAwaitableMixinFactory.confirmingAction<ConfirmationDialogInfo>()
|
||||
|
||||
val selectedWalletModel = selectedAccountUseCase.selectedWalletModelFlow()
|
||||
.shareInBackground()
|
||||
|
||||
val selectedCurrencyFlow = currencyInteractor.observeSelectCurrency()
|
||||
.map { mapCurrencyToUI(it) }
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val selectedLanguageFlow = flowOf {
|
||||
languageUseCase.selectedLanguageModel()
|
||||
}
|
||||
.shareInBackground()
|
||||
|
||||
val appVersionFlow = flowOf {
|
||||
resourceManager.getString(R.string.about_version_template, appVersionProvider.versionName)
|
||||
}
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val pinCodeVerificationStatus = twoFactorVerificationService.isEnabledFlow()
|
||||
|
||||
private val walletConnectSessions = walletConnectSessionsUseCase.activeSessionsNumberFlow()
|
||||
.shareInBackground()
|
||||
|
||||
val walletConnectSessionsUi = walletConnectSessions
|
||||
.map(::mapNumberOfActiveSessionsToUi)
|
||||
.shareInBackground()
|
||||
|
||||
val safeModeStatus = safeModeService.safeModeStatusFlow()
|
||||
|
||||
val hideBalancesOnLaunchState = maskingModeUseCase.observeHideBalancesOnLaunchEnabled()
|
||||
|
||||
override val openBrowserEvent = MutableLiveData<Event<String>>()
|
||||
|
||||
private val _openEmailEvent = MutableLiveData<Event<String>>()
|
||||
val openEmailEvent: LiveData<Event<String>> = _openEmailEvent
|
||||
|
||||
val biometricAuthStatus = biometricService.isEnabledFlow()
|
||||
|
||||
val biometricEventMessages = biometricService.biometryServiceResponseFlow
|
||||
.mapNotNull { mapBiometricErrors(resourceManager, it) }
|
||||
.shareInBackground()
|
||||
.asLiveData()
|
||||
|
||||
val showBiometricNotReadyDialogEvent = biometricService.biometryServiceResponseFlow
|
||||
.filterIsInstance<BiometricResponse.NotReady>()
|
||||
.map { Event(true) }
|
||||
.asLiveData()
|
||||
|
||||
val pushNotificationsState = pushNotificationsInteractor.pushNotificationsEnabledFlow()
|
||||
.map { resourceManager.formatBooleanToState(it) }
|
||||
.shareInBackground()
|
||||
|
||||
init {
|
||||
setupBiometric()
|
||||
}
|
||||
|
||||
fun walletsClicked() {
|
||||
router.openWallets()
|
||||
}
|
||||
|
||||
fun pushNotificationsClicked() {
|
||||
when (pushNotificationsInteractor.pushNotificationsAvailabilityState()) {
|
||||
PushNotificationsAvailabilityState.AVAILABLE -> router.openPushNotificationSettings()
|
||||
|
||||
PushNotificationsAvailabilityState.PLAY_SERVICES_REQUIRED -> {
|
||||
showError(
|
||||
resourceManager.getString(R.string.common_not_available),
|
||||
resourceManager.getString(R.string.settings_push_notifications_only_available_with_google_services_error)
|
||||
)
|
||||
}
|
||||
|
||||
PushNotificationsAvailabilityState.GOOGLE_PLAY_INSTALLATION_REQUIRED -> {
|
||||
showError(
|
||||
resourceManager.getString(R.string.common_not_available),
|
||||
resourceManager.getString(R.string.settings_push_notifications_only_available_from_google_play_error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun currenciesClicked() {
|
||||
router.openCurrencies()
|
||||
}
|
||||
|
||||
fun languagesClicked() {
|
||||
router.openLanguages()
|
||||
}
|
||||
|
||||
fun appearanceClicked() {
|
||||
router.openAppearance()
|
||||
}
|
||||
|
||||
fun changeBiometricAuth() {
|
||||
launch {
|
||||
if (biometricService.isEnabled()) {
|
||||
val confirmationResult = twoFactorVerificationService.requestConfirmation(useBiometry = false)
|
||||
if (confirmationResult == TwoFactorVerificationResult.CONFIRMED) {
|
||||
biometricService.toggle()
|
||||
}
|
||||
} else {
|
||||
biometricService.requestBiometric()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun changePincodeVerification() {
|
||||
launch {
|
||||
if (!twoFactorVerificationService.isEnabled()) {
|
||||
confirmationAwaitableAction.awaitAction(
|
||||
ConfirmationDialogInfo.fromRes(
|
||||
resourceManager,
|
||||
R.string.settings_pin_code_verification_confirmation_title,
|
||||
R.string.settings_pin_code_verification_confirmation_message,
|
||||
R.string.common_enable,
|
||||
R.string.common_cancel
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
twoFactorVerificationService.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeSafeMode() {
|
||||
launch {
|
||||
if (!safeModeService.isSafeModeEnabled()) {
|
||||
confirmationAwaitableAction.awaitAction(
|
||||
ConfirmationDialogInfo.fromRes(
|
||||
resourceManager,
|
||||
R.string.settings_safe_mode_confirmation_title,
|
||||
R.string.settings_safe_mode_confirmation_message,
|
||||
R.string.common_enable,
|
||||
R.string.common_cancel
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
safeModeService.toggleSafeMode()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeHideBalances() {
|
||||
maskingModeUseCase.toggleHideBalancesOnLaunch()
|
||||
}
|
||||
|
||||
fun changePinCodeClicked() {
|
||||
router.openChangePinCode()
|
||||
}
|
||||
|
||||
fun telegramClicked() {
|
||||
openLink(appLinksProvider.telegram)
|
||||
}
|
||||
|
||||
fun twitterClicked() {
|
||||
openLink(appLinksProvider.twitter)
|
||||
}
|
||||
|
||||
fun rateUsClicked() {
|
||||
openLink(appLinksProvider.rateApp)
|
||||
}
|
||||
|
||||
fun wikiClicked() {
|
||||
openLink(appLinksProvider.wikiBase)
|
||||
}
|
||||
|
||||
fun websiteClicked() {
|
||||
openLink(appLinksProvider.website)
|
||||
}
|
||||
|
||||
fun githubClicked() {
|
||||
openLink(appLinksProvider.github)
|
||||
}
|
||||
|
||||
fun termsClicked() {
|
||||
openLink(appLinksProvider.termsUrl)
|
||||
}
|
||||
|
||||
fun privacyClicked() {
|
||||
openLink(appLinksProvider.privacyUrl)
|
||||
}
|
||||
|
||||
fun emailClicked() {
|
||||
_openEmailEvent.value = appLinksProvider.email.event()
|
||||
}
|
||||
|
||||
fun openYoutube() {
|
||||
openLink(appLinksProvider.youtube)
|
||||
}
|
||||
|
||||
fun accountActionsClicked() = launch {
|
||||
val selectedWalletId = selectedAccountUseCase.getSelectedMetaAccount().id
|
||||
|
||||
router.openWalletDetails(selectedWalletId)
|
||||
}
|
||||
|
||||
fun selectedWalletClicked() {
|
||||
router.openSwitchWallet()
|
||||
}
|
||||
|
||||
fun networksClicked() {
|
||||
router.openNetworks()
|
||||
}
|
||||
|
||||
fun walletConnectClicked() = launch {
|
||||
if (walletConnectSessions.first() > 0) {
|
||||
router.openWalletConnectSessions()
|
||||
} else {
|
||||
router.openWalletConnectScan()
|
||||
}
|
||||
}
|
||||
|
||||
fun cloudBackupClicked() {
|
||||
router.openCloudBackupSettings()
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
biometricService.refreshBiometryState()
|
||||
}
|
||||
|
||||
fun onPause() {
|
||||
biometricService.cancel()
|
||||
}
|
||||
|
||||
private fun openLink(link: String) {
|
||||
openBrowserEvent.value = link.event()
|
||||
}
|
||||
|
||||
private fun setupBiometric() {
|
||||
biometricService.biometryServiceResponseFlow
|
||||
.filterIsInstance<BiometricResponse.Success>()
|
||||
.onEach { biometricService.toggle() }
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.settings.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
SettingsModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface SettingsComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment
|
||||
): SettingsComponent
|
||||
}
|
||||
|
||||
fun inject(settingsFragment: io.novafoundation.nova.feature_settings_impl.presentation.settings.SettingsFragment)
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.settings.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.data.network.AppLinksProvider
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.domain.usecase.MaskingModeUseCase
|
||||
import io.novafoundation.nova.common.io.MainThreadExecutor
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.resources.AppVersionProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.sequrity.SafeModeService
|
||||
import io.novafoundation.nova.common.sequrity.TwoFactorVerificationService
|
||||
import io.novafoundation.nova.common.sequrity.biometry.BiometricPromptFactory
|
||||
import io.novafoundation.nova.common.sequrity.biometry.BiometricService
|
||||
import io.novafoundation.nova.common.sequrity.biometry.BiometricServiceFactory
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.language.LanguageUseCase
|
||||
import io.novafoundation.nova.feature_currency_api.domain.CurrencyInteractor
|
||||
import io.novafoundation.nova.feature_push_notifications.domain.interactor.PushNotificationsInteractor
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.SettingsRouter
|
||||
import io.novafoundation.nova.feature_settings_impl.presentation.settings.SettingsViewModel
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.domain.sessions.WalletConnectSessionsUseCase
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class SettingsModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(SettingsViewModel::class)
|
||||
fun provideViewModel(
|
||||
languageUseCase: LanguageUseCase,
|
||||
router: SettingsRouter,
|
||||
appLinksProvider: AppLinksProvider,
|
||||
resourceManager: ResourceManager,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
currencyInteractor: CurrencyInteractor,
|
||||
safeModeService: SafeModeService,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
walletConnectSessionsUseCase: WalletConnectSessionsUseCase,
|
||||
twoFactorVerificationService: TwoFactorVerificationService,
|
||||
biometricService: BiometricService,
|
||||
pushNotificationsInteractor: PushNotificationsInteractor,
|
||||
maskingModeUseCase: MaskingModeUseCase
|
||||
): ViewModel {
|
||||
return SettingsViewModel(
|
||||
languageUseCase,
|
||||
router,
|
||||
appLinksProvider,
|
||||
resourceManager,
|
||||
appVersionProvider,
|
||||
selectedAccountUseCase,
|
||||
currencyInteractor,
|
||||
safeModeService,
|
||||
actionAwaitableMixinFactory,
|
||||
walletConnectSessionsUseCase,
|
||||
twoFactorVerificationService,
|
||||
biometricService,
|
||||
pushNotificationsInteractor,
|
||||
maskingModeUseCase
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): SettingsViewModel {
|
||||
return ViewModelProvider(fragment, viewModelFactory).get(SettingsViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideBiometricService(
|
||||
fragment: Fragment,
|
||||
context: Context,
|
||||
resourceManager: ResourceManager,
|
||||
biometricServiceFactory: BiometricServiceFactory
|
||||
): BiometricService {
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
val biometricPromptFactory = BiometricPromptFactory(fragment, MainThreadExecutor())
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(resourceManager.getString(R.string.biometric_auth_title))
|
||||
.setSubtitle(resourceManager.getString(R.string.pincode_biometry_dialog_subtitle))
|
||||
.setNegativeButtonText(resourceManager.getString(R.string.common_cancel))
|
||||
.build()
|
||||
|
||||
return biometricServiceFactory.create(biometricManager, biometricPromptFactory, promptInfo)
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package io.novafoundation.nova.feature_settings_impl.presentation.settings.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import io.novafoundation.nova.common.utils.WithContextExtensions
|
||||
import io.novafoundation.nova.common.utils.getDrawableCompat
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setDrawableStart
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.common.utils.setTextOrHide
|
||||
import io.novafoundation.nova.common.utils.themed
|
||||
import io.novafoundation.nova.feature_settings_impl.R
|
||||
import io.novafoundation.nova.feature_settings_impl.databinding.ViewWalletConnectItemBinding
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectSessionsModel
|
||||
|
||||
class WalletConnectItemView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val binder = ViewWalletConnectItemBinding.inflate(inflater(), this)
|
||||
|
||||
init {
|
||||
orientation = HORIZONTAL
|
||||
background = context.getDrawableCompat(R.drawable.bg_primary_list_item)
|
||||
}
|
||||
|
||||
fun setValue(value: WalletConnectSessionsModel) {
|
||||
binder.walletConnectItemValue.setModel(value)
|
||||
}
|
||||
}
|
||||
|
||||
class WalletConnectConnectionsChip @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : AppCompatTextView(context.themed(R.style.TextAppearance_NovaFoundation_Regular_Footnote), attrs, defStyleAttr),
|
||||
WithContextExtensions by WithContextExtensions(context) {
|
||||
|
||||
init {
|
||||
setTextColorRes(R.color.chip_text)
|
||||
setDrawableStart(R.drawable.ic_connections, widthInDp = 12, paddingInDp = 4, tint = R.color.chip_icon)
|
||||
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
includeFontPadding = false
|
||||
|
||||
setPadding(8.dp, 4.dp, 8.dp, 4.dp)
|
||||
|
||||
background = getRoundedCornerDrawable(R.color.chips_background, cornerSizeDp = 8)
|
||||
}
|
||||
}
|
||||
|
||||
fun WalletConnectConnectionsChip.setModel(model: WalletConnectSessionsModel) {
|
||||
setTextOrHide(model.connections)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user