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:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+55
View File
@@ -0,0 +1,55 @@
apply plugin: 'kotlin-parcelize'
android {
defaultConfig {
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
namespace 'io.novafoundation.nova.feature_ledger_impl'
buildFeatures {
viewBinding true
}
}
dependencies {
implementation project(":feature-ledger-api")
implementation project(":feature-ledger-core")
implementation project(':feature-account-api')
implementation project(':feature-wallet-api')
implementation project(":common")
implementation project(":runtime")
implementation materialDep
implementation substrateSdkDep
implementation bleDep
implementation bleKotlinDep
implementation kotlinDep
implementation androidDep
implementation permissionsDep
implementation coroutinesDep
implementation coroutinesAndroidDep
implementation lifeCycleKtxDep
implementation project(":core-db")
implementation daggerDep
ksp daggerCompiler
testImplementation jUnitDep
testImplementation mockitoDep
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_ledger_impl.data.repository
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
import io.novafoundation.nova.feature_ledger_api.data.repository.LedgerDerivationPath
import io.novafoundation.nova.feature_ledger_api.data.repository.LedgerRepository
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
class RealLedgerRepository(
private val secretStoreV2: SecretStoreV2,
) : LedgerRepository {
override suspend fun getChainAccountDerivationPath(metaId: Long, chainId: ChainId): String {
val key = LedgerDerivationPath.legacyDerivationPathSecretKey(chainId)
return secretStoreV2.getAdditionalMetaAccountSecret(metaId, key)
?: throw IllegalStateException("Cannot find Ledger derivation path for chain $chainId in meta account $metaId")
}
override suspend fun getGenericDerivationPath(metaId: Long): String {
val key = LedgerDerivationPath.genericDerivationPathSecretKey()
return secretStoreV2.getAdditionalMetaAccountSecret(metaId, key)
?: throw IllegalStateException("Cannot find Ledger generic derivation path for meta account $metaId")
}
}
@@ -0,0 +1,90 @@
package io.novafoundation.nova.feature_ledger_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.core_db.di.DbApi
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
import io.novafoundation.nova.feature_account_api.presenatation.sign.LedgerSignCommunicator
import io.novafoundation.nova.feature_ledger_api.di.LedgerFeatureApi
import io.novafoundation.nova.feature_ledger_core.di.LedgerCoreApi
import io.novafoundation.nova.feature_ledger_impl.di.modules.LedgerBindsModule
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress.di.AddEvmGenericLedgerAccountSelectAddressComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectLedger.di.AddEvmAccountSelectGenericLedgerComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectAddress.di.AddLedgerChainAccountSelectAddressComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectLedger.di.AddChainAccountSelectLedgerComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.finish.di.FinishImportGenericLedgerComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.preview.di.PreviewImportGenericLedgerComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectAddress.di.SelectAddressImportGenericLedgerComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectLedger.di.SelectLedgerGenericImportComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.start.di.StartImportGenericLedgerComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.legacy.SelectLedgerAddressInterScreenCommunicator
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.legacy.fillWallet.di.FillWalletImportLedgerComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.legacy.finish.di.FinishImportLedgerComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.legacy.selectAddress.di.SelectAddressImportLedgerLegacyComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.legacy.selectLedger.di.SelectLedgerImportLedgerComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.legacy.start.di.StartImportLegacyLedgerComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.sign.di.SignLedgerComponent
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.runtime.di.RuntimeApi
@Component(
dependencies = [
LedgerFeatureDependencies::class,
],
modules = [
LedgerFeatureModule::class,
LedgerBindsModule::class
]
)
@FeatureScope
interface LedgerFeatureComponent : LedgerFeatureApi {
@Component.Factory
interface Factory {
fun create(
deps: LedgerFeatureDependencies,
@BindsInstance router: LedgerRouter,
@BindsInstance selectLedgerAddressInterScreenCommunicator: SelectLedgerAddressInterScreenCommunicator,
@BindsInstance signInterScreenCommunicator: LedgerSignCommunicator,
): LedgerFeatureComponent
}
fun startImportLegacyLedgerComponentFactory(): StartImportLegacyLedgerComponent.Factory
fun fillWalletImportLedgerComponentFactory(): FillWalletImportLedgerComponent.Factory
fun selectLedgerImportComponentFactory(): SelectLedgerImportLedgerComponent.Factory
fun selectAddressImportLedgerLegacyComponentFactory(): SelectAddressImportLedgerLegacyComponent.Factory
fun selectAddressImportLedgerGenericComponentFactory(): SelectAddressImportGenericLedgerComponent.Factory
fun finishImportLedgerComponentFactory(): FinishImportLedgerComponent.Factory
fun signLedgerComponentFactory(): SignLedgerComponent.Factory
fun addChainAccountSelectLedgerComponentFactory(): AddChainAccountSelectLedgerComponent.Factory
fun addChainAccountSelectAddressComponentFactory(): AddLedgerChainAccountSelectAddressComponent.Factory
// New generic app flow
fun startImportGenericLedgerComponentFactory(): StartImportGenericLedgerComponent.Factory
fun selectLedgerGenericImportComponentFactory(): SelectLedgerGenericImportComponent.Factory
fun previewImportGenericLedgerComponentFactory(): PreviewImportGenericLedgerComponent.Factory
fun finishGenericImportLedgerComponentFactory(): FinishImportGenericLedgerComponent.Factory
// Generic import EVM account
fun addEvmAccountSelectGenericLedgerComponentFactory(): AddEvmAccountSelectGenericLedgerComponent.Factory
fun addEvmGenericLedgerAccountSelectAddressComponentFactory(): AddEvmGenericLedgerAccountSelectAddressComponent.Factory
@Component(
dependencies = [
CommonApi::class,
RuntimeApi::class,
WalletFeatureApi::class,
AccountFeatureApi::class,
LedgerCoreApi::class,
DbApi::class,
]
)
interface LedgerFeatureDependenciesComponent : LedgerFeatureDependencies
}
@@ -0,0 +1,92 @@
package io.novafoundation.nova.feature_ledger_impl.di
import coil.ImageLoader
import io.novafoundation.nova.common.address.AddressIconGenerator
import io.novafoundation.nova.common.address.format.AddressSchemeFormatter
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
import io.novafoundation.nova.common.resources.ContextManager
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.bluetooth.BluetoothManager
import io.novafoundation.nova.common.utils.location.LocationManager
import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory
import io.novafoundation.nova.core_db.dao.MetaAccountDao
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.GenericLedgerAddAccountRepository
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LegacyLedgerAddAccountRepository
import io.novafoundation.nova.feature_account_api.data.signer.SigningSharedState
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.actions.ExternalActions
import io.novafoundation.nova.feature_account_api.presenatation.addressActions.AddressActionsMixin
import io.novafoundation.nova.feature_ledger_core.domain.LedgerMigrationTracker
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
import io.novafoundation.nova.runtime.extrinsic.ExtrinsicValidityUseCase
import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.network.rpc.RpcCalls
interface LedgerFeatureDependencies {
val amountFormatter: AmountFormatter
val chainRegistry: ChainRegistry
val appLinksProvider: AppLinksProvider
val imageLoader: ImageLoader
val addressIconGenerator: AddressIconGenerator
val resourceManager: ResourceManager
val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
val bluetoothManager: BluetoothManager
val locationManager: LocationManager
val permissionAskerFactory: PermissionsAskerFactory
val contextManager: ContextManager
val assetSourceRegistry: AssetSourceRegistry
val tokenRepository: TokenRepository
val metaAccountDao: MetaAccountDao
val accountInteractor: AccountInteractor
val accountRepository: AccountRepository
val secretStoreV2: SecretStoreV2
val signSharedState: SigningSharedState
val extrinsicValidityUseCase: ExtrinsicValidityUseCase
val selectedAccountUseCase: SelectedAccountUseCase
val legacyLedgerAddAccountRepository: LegacyLedgerAddAccountRepository
val genericLegacyLedgerAddAccountRepository: GenericLedgerAddAccountRepository
val apiCreator: NetworkApiCreator
val rpcCalls: RpcCalls
val metadataShortenerService: MetadataShortenerService
val ledgerMigrationTracker: LedgerMigrationTracker
val externalActions: ExternalActions.Presentation
val addressActionsMixinFactory: AddressActionsMixin.Factory
val addressSchemeFormatter: AddressSchemeFormatter
}
@@ -0,0 +1,42 @@
package io.novafoundation.nova.feature_ledger_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.core_db.di.DbApi
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
import io.novafoundation.nova.feature_account_api.presenatation.sign.LedgerSignCommunicator
import io.novafoundation.nova.feature_ledger_core.di.LedgerCoreApi
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.legacy.SelectLedgerAddressInterScreenCommunicator
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.runtime.di.RuntimeApi
import javax.inject.Inject
@ApplicationScope
class LedgerFeatureHolder @Inject constructor(
featureContainer: FeatureContainer,
private val router: LedgerRouter,
private val selectLedgerAddressInterScreenCommunicator: SelectLedgerAddressInterScreenCommunicator,
private val signInterScreenCommunicator: LedgerSignCommunicator,
) : FeatureApiHolder(featureContainer) {
override fun initializeDependencies(): Any {
val accountFeatureDependencies = DaggerLedgerFeatureComponent_LedgerFeatureDependenciesComponent.builder()
.commonApi(commonApi())
.dbApi(getFeature(DbApi::class.java))
.runtimeApi(getFeature(RuntimeApi::class.java))
.walletFeatureApi(getFeature(WalletFeatureApi::class.java))
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
.ledgerCoreApi(getFeature(LedgerCoreApi::class.java))
.build()
return DaggerLedgerFeatureComponent.factory()
.create(
accountFeatureDependencies,
router,
selectLedgerAddressInterScreenCommunicator,
signInterScreenCommunicator
)
}
}
@@ -0,0 +1,168 @@
package io.novafoundation.nova.feature_ledger_impl.di
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.address.format.AddressSchemeFormatter
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.resources.ContextManager
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.bluetooth.BluetoothManager
import io.novafoundation.nova.feature_ledger_api.data.repository.LedgerRepository
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_api.sdk.transport.LedgerTransport
import io.novafoundation.nova.feature_ledger_core.domain.LedgerMigrationTracker
import io.novafoundation.nova.feature_ledger_impl.data.repository.RealLedgerRepository
import io.novafoundation.nova.feature_ledger_impl.di.modules.GenericLedgerModule
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.RealSelectAddressLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.SelectAddressLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.domain.migration.LedgerMigrationUseCase
import io.novafoundation.nova.feature_ledger_impl.domain.migration.RealLedgerMigrationUseCase
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessagePresentable
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatterFactory
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.SingleSheetLedgerMessagePresentable
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers.LedgerDeviceFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatterFactory
import io.novafoundation.nova.feature_ledger_impl.sdk.application.substrate.legacyApp.LegacySubstrateLedgerApplication
import io.novafoundation.nova.feature_ledger_impl.sdk.application.substrate.newApp.GenericSubstrateLedgerApplication
import io.novafoundation.nova.feature_ledger_impl.sdk.application.substrate.newApp.MigrationSubstrateLedgerApplication
import io.novafoundation.nova.feature_ledger_impl.sdk.connection.ble.LedgerBleManager
import io.novafoundation.nova.feature_ledger_impl.sdk.discovery.CompoundLedgerDiscoveryService
import io.novafoundation.nova.feature_ledger_impl.sdk.discovery.ble.BleLedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_impl.sdk.discovery.usb.UsbLedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_impl.sdk.transport.ChunkedLedgerTransport
import io.novafoundation.nova.runtime.extrinsic.metadata.MetadataShortenerService
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
@Module(includes = [GenericLedgerModule::class])
class LedgerFeatureModule {
@Provides
@FeatureScope
fun provideLedgerTransport(): LedgerTransport = ChunkedLedgerTransport()
@Provides
@FeatureScope
fun provideSubstrateLedgerApplication(
transport: LedgerTransport,
ledgerRepository: LedgerRepository,
) = LegacySubstrateLedgerApplication(transport, ledgerRepository)
@Provides
@FeatureScope
fun provideMigrationLedgerApplication(
transport: LedgerTransport,
chainRegistry: ChainRegistry,
ledgerRepository: LedgerRepository,
metadataShortenerService: MetadataShortenerService
) = MigrationSubstrateLedgerApplication(
transport = transport,
chainRegistry = chainRegistry,
metadataShortenerService = metadataShortenerService,
ledgerRepository = ledgerRepository
)
@Provides
@FeatureScope
fun provideGenericLedgerApplication(
transport: LedgerTransport,
chainRegistry: ChainRegistry,
ledgerRepository: LedgerRepository,
metadataShortenerService: MetadataShortenerService
) = GenericSubstrateLedgerApplication(
transport = transport,
metadataShortenerService = metadataShortenerService,
ledgerRepository = ledgerRepository,
chainRegistry = chainRegistry
)
@Provides
@FeatureScope
fun provideLedgerMessageFormatterFactory(
resourceManager: ResourceManager,
migrationTracker: LedgerMigrationTracker,
chainRegistry: ChainRegistry,
appLinksProvider: AppLinksProvider,
): LedgerMessageFormatterFactory {
return LedgerMessageFormatterFactory(resourceManager, migrationTracker, chainRegistry, appLinksProvider)
}
@Provides
@FeatureScope
fun provideLedgerMigrationUseCase(
ledgerMigrationTracker: LedgerMigrationTracker,
migrationApp: MigrationSubstrateLedgerApplication,
legacyApp: LegacySubstrateLedgerApplication,
genericApp: GenericSubstrateLedgerApplication,
): LedgerMigrationUseCase {
return RealLedgerMigrationUseCase(ledgerMigrationTracker, migrationApp, legacyApp, genericApp)
}
@Provides
@FeatureScope
fun provideLedgerBleManager(
contextManager: ContextManager
) = LedgerBleManager(contextManager)
@Provides
@FeatureScope
fun provideLedgerDeviceDiscoveryService(
bluetoothManager: BluetoothManager,
ledgerBleManager: LedgerBleManager
) = BleLedgerDeviceDiscoveryService(
bluetoothManager = bluetoothManager,
ledgerBleManager = ledgerBleManager
)
@Provides
@FeatureScope
fun provideUsbDeviceDiscoveryService(
contextManager: ContextManager
) = UsbLedgerDeviceDiscoveryService(contextManager)
@Provides
@FeatureScope
fun provideDeviceDiscoveryService(
bleLedgerDeviceDiscoveryService: BleLedgerDeviceDiscoveryService,
usbLedgerDeviceDiscoveryService: UsbLedgerDeviceDiscoveryService
): LedgerDeviceDiscoveryService = CompoundLedgerDiscoveryService(
bleLedgerDeviceDiscoveryService,
usbLedgerDeviceDiscoveryService
)
@Provides
@FeatureScope
fun provideRepository(
secretStoreV2: SecretStoreV2
): LedgerRepository = RealLedgerRepository(secretStoreV2)
@Provides
fun provideLedgerMessagePresentable(): LedgerMessagePresentable = SingleSheetLedgerMessagePresentable()
@Provides
@FeatureScope
fun provideSelectAddressInteractor(
migrationUseCase: LedgerMigrationUseCase,
ledgerDeviceDiscoveryService: LedgerDeviceDiscoveryService,
): SelectAddressLedgerInteractor {
return RealSelectAddressLedgerInteractor(
migrationUseCase = migrationUseCase,
ledgerDeviceDiscoveryService = ledgerDeviceDiscoveryService,
)
}
@Provides
@FeatureScope
fun provideLedgerDeviceMapper(resourceManager: ResourceManager): LedgerDeviceFormatter {
return LedgerDeviceFormatter(resourceManager)
}
@Provides
@FeatureScope
fun provideMessageCommandFormatterFactory(
resourceManager: ResourceManager,
deviceMapper: LedgerDeviceFormatter,
addressSchemeFormatter: AddressSchemeFormatter
) = MessageCommandFormatterFactory(resourceManager, deviceMapper, addressSchemeFormatter)
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_ledger_impl.di.annotations
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.SOURCE)
annotation class GenericLedger
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_ledger_impl.di.modules
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_ledger_impl.di.annotations.GenericLedger
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatterFactory
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatterFactory
@Module
class GenericLedgerModule {
@Provides
@FeatureScope
@GenericLedger
fun provideMessageFormatter(factory: LedgerMessageFormatterFactory): LedgerMessageFormatter = factory.createGeneric()
@Provides
@FeatureScope
@GenericLedger
fun provideMessageCommandFormatter(
@GenericLedger messageFormatter: LedgerMessageFormatter,
messageCommandFormatterFactory: MessageCommandFormatterFactory
): MessageCommandFormatter = messageCommandFormatterFactory.create(messageFormatter)
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_ledger_impl.di.modules
import dagger.Binds
import dagger.Module
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.generic.GenericLedgerEvmAlertFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.generic.RealGenericLedgerEvmAlertFormatter
@Module
interface LedgerBindsModule {
@Binds
fun bindEvmUpdateFormatter(real: RealGenericLedgerEvmAlertFormatter): GenericLedgerEvmAlertFormatter
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_ledger_impl.domain.account.addChain.generic
import io.novafoundation.nova.common.di.scope.ScreenScope
import io.novafoundation.nova.common.utils.coerceToUnit
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.GenericLedgerAddAccountRepository
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerEvmAccount
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
interface AddEvmAccountToGenericLedgerInteractor {
suspend fun addEvmAccount(metaId: Long, account: LedgerEvmAccount): Result<Unit>
}
@ScreenScope
class RealAddEvmAccountToGenericLedgerInteractor @Inject constructor(
private val genericLedgerAddAccountRepository: GenericLedgerAddAccountRepository
) : AddEvmAccountToGenericLedgerInteractor {
override suspend fun addEvmAccount(metaId: Long, account: LedgerEvmAccount): Result<Unit> = withContext(Dispatchers.IO) {
runCatching {
val payload = GenericLedgerAddAccountRepository.Payload.AddEvmAccount(metaId, account)
genericLedgerAddAccountRepository.addAccount(payload)
}.coerceToUnit()
}
}
@@ -0,0 +1,24 @@
package io.novafoundation.nova.feature_ledger_impl.domain.account.addChain.legacy
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LegacyLedgerAddAccountRepository
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerSubstrateAccount
interface AddLedgerChainAccountInteractor {
suspend fun addChainAccount(metaId: Long, chainId: String, account: LedgerSubstrateAccount): Result<Unit>
}
class RealAddLedgerChainAccountInteractor(
private val legacyLedgerAddAccountRepository: LegacyLedgerAddAccountRepository
) : AddLedgerChainAccountInteractor {
override suspend fun addChainAccount(metaId: Long, chainId: String, account: LedgerSubstrateAccount): Result<Unit> = kotlin.runCatching {
legacyLedgerAddAccountRepository.addAccount(
LegacyLedgerAddAccountRepository.Payload.ChainAccount(
metaId,
chainId,
account
)
)
}
}
@@ -0,0 +1,77 @@
package io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.feature_account_api.domain.model.LedgerVariant
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerEvmAccount
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerSubstrateAccount
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.findDeviceOrThrow
import io.novafoundation.nova.feature_ledger_impl.domain.migration.LedgerMigrationUseCase
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
class LedgerAccount(
val index: Int,
val substrate: LedgerSubstrateAccount,
val evm: LedgerEvmAccount?,
)
interface SelectAddressLedgerInteractor {
suspend fun getDevice(deviceId: String): LedgerDevice
suspend fun loadLedgerAccount(substrateChain: Chain, deviceId: String, accountIndex: Int, ledgerVariant: LedgerVariant): Result<LedgerAccount>
suspend fun verifyLedgerAccount(
substrateChain: Chain,
deviceId: String,
accountIndex: Int,
ledgerVariant: LedgerVariant,
addressSchemes: List<AddressScheme>
): Result<Unit>
}
class RealSelectAddressLedgerInteractor(
private val migrationUseCase: LedgerMigrationUseCase,
private val ledgerDeviceDiscoveryService: LedgerDeviceDiscoveryService,
) : SelectAddressLedgerInteractor {
override suspend fun getDevice(deviceId: String): LedgerDevice {
return ledgerDeviceDiscoveryService.findDeviceOrThrow(deviceId)
}
override suspend fun loadLedgerAccount(
substrateChain: Chain,
deviceId: String,
accountIndex: Int,
ledgerVariant: LedgerVariant,
) = runCatching {
val device = ledgerDeviceDiscoveryService.findDeviceOrThrow(deviceId)
val app = migrationUseCase.determineLedgerApp(substrateChain.id, ledgerVariant)
val substrateAccount = app.getSubstrateAccount(device, substrateChain.id, accountIndex, confirmAddress = false)
val evmAccount = app.getEvmAccount(device, accountIndex, confirmAddress = false)
LedgerAccount(accountIndex, substrateAccount, evmAccount)
}
override suspend fun verifyLedgerAccount(
substrateChain: Chain,
deviceId: String,
accountIndex: Int,
ledgerVariant: LedgerVariant,
addressSchemes: List<AddressScheme>
): Result<Unit> = runCatching {
val device = ledgerDeviceDiscoveryService.findDeviceOrThrow(deviceId)
val app = migrationUseCase.determineLedgerApp(substrateChain.id, ledgerVariant)
val verificationPerScheme = mapOf(
AddressScheme.SUBSTRATE to suspend { app.getSubstrateAccount(device, substrateChain.id, accountIndex, confirmAddress = true) },
AddressScheme.EVM to suspend { app.getEvmAccount(device, accountIndex, confirmAddress = true) }
)
addressSchemes.forEach {
verificationPerScheme.getValue(it).invoke()
}
}
}
@@ -0,0 +1,38 @@
package io.novafoundation.nova.feature_ledger_impl.domain.account.connect.generic.finish
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.addAccountWithSingleChange
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.GenericLedgerAddAccountRepository
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerEvmAccount
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerSubstrateAccount
interface FinishImportGenericLedgerInteractor {
suspend fun createWallet(
name: String,
substrateAccount: LedgerSubstrateAccount,
evmAccount: LedgerEvmAccount?,
): Result<Unit>
}
class RealFinishImportGenericLedgerInteractor(
private val genericLedgerAddAccountRepository: GenericLedgerAddAccountRepository,
private val accountRepository: AccountRepository,
) : FinishImportGenericLedgerInteractor {
override suspend fun createWallet(
name: String,
substrateAccount: LedgerSubstrateAccount,
evmAccount: LedgerEvmAccount?,
) = runCatching {
val payload = GenericLedgerAddAccountRepository.Payload.NewWallet(
name = name,
substrateAccount = substrateAccount,
evmAccount = evmAccount
)
val addAccountResult = genericLedgerAddAccountRepository.addAccountWithSingleChange(payload)
accountRepository.selectMetaAccount(addAccountResult.metaId)
}
}
@@ -0,0 +1,69 @@
package io.novafoundation.nova.feature_ledger_impl.domain.account.connect.generic.preview
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.common.list.GroupedList
import io.novafoundation.nova.common.utils.mapValuesNotNull
import io.novafoundation.nova.feature_account_api.presenatation.account.chain.preview.model.ChainAccountPreview
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.findDeviceOrThrow
import io.novafoundation.nova.feature_ledger_core.domain.LedgerMigrationTracker
import io.novafoundation.nova.feature_ledger_impl.sdk.application.substrate.newApp.GenericSubstrateLedgerApplication
import io.novafoundation.nova.runtime.ext.addressScheme
import io.novafoundation.nova.runtime.ext.defaultComparator
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
interface PreviewImportGenericLedgerInteractor {
suspend fun getDevice(deviceId: String): LedgerDevice
suspend fun availableChainAccounts(
substrateAccountId: AccountId,
evmAccountId: AccountId?,
): GroupedList<AddressScheme, ChainAccountPreview>
suspend fun verifyAddressOnLedger(accountIndex: Int, deviceId: String): Result<Unit>
}
class RealPreviewImportGenericLedgerInteractor(
private val ledgerMigrationTracker: LedgerMigrationTracker,
private val genericSubstrateLedgerApplication: GenericSubstrateLedgerApplication,
private val ledgerDiscoveryService: LedgerDeviceDiscoveryService
) : PreviewImportGenericLedgerInteractor {
override suspend fun getDevice(deviceId: String): LedgerDevice {
return ledgerDiscoveryService.findDeviceOrThrow(deviceId)
}
override suspend fun availableChainAccounts(
substrateAccountId: AccountId,
evmAccountId: AccountId?,
): GroupedList<AddressScheme, ChainAccountPreview> {
return ledgerMigrationTracker.supportedChainsByGenericApp()
.groupBy(Chain::addressScheme)
.mapValuesNotNull { (scheme, chains) ->
val accountId = when (scheme) {
AddressScheme.EVM -> evmAccountId ?: return@mapValuesNotNull null
AddressScheme.SUBSTRATE -> substrateAccountId
}
chains
.sortedWith(Chain.defaultComparator())
.map { chain -> ChainAccountPreview(chain, accountId) }
}
}
override suspend fun verifyAddressOnLedger(accountIndex: Int, deviceId: String): Result<Unit> = withContext(Dispatchers.IO) {
runCatching {
val device = ledgerDiscoveryService.findDeviceOrThrow(deviceId)
genericSubstrateLedgerApplication.getUniversalSubstrateAccount(device, accountIndex, confirmAddress = true)
genericSubstrateLedgerApplication.getEvmAccount(device, accountIndex, confirmAddress = true)
Unit
}
}
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_ledger_impl.domain.account.connect.legacy.fillWallet
import io.novafoundation.nova.common.utils.mapToSet
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.SubstrateApplicationConfig
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.enabledChainById
interface FillWalletImportLedgerInteractor {
suspend fun availableLedgerChains(): List<Chain>
}
class RealFillWalletImportLedgerInteractor(
private val chainRegistry: ChainRegistry
) : FillWalletImportLedgerInteractor {
override suspend fun availableLedgerChains(): List<Chain> {
val supportedLedgerApps = SubstrateApplicationConfig.all()
val supportedChainIds = supportedLedgerApps.mapToSet { it.chainId }
return chainRegistry.enabledChainById()
.filterKeys { it in supportedChainIds }
.values
.toList()
}
}
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_ledger_impl.domain.account.connect.legacy.finish
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.addAccountWithSingleChange
import io.novafoundation.nova.feature_account_api.data.repository.addAccount.ledger.LegacyLedgerAddAccountRepository
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerSubstrateAccount
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
interface FinishImportLedgerInteractor {
suspend fun createWallet(
name: String,
ledgerChainAccounts: Map<ChainId, LedgerSubstrateAccount>,
): Result<Unit>
}
class RealFinishImportLedgerInteractor(
private val legacyLedgerAddAccountRepository: LegacyLedgerAddAccountRepository,
private val accountRepository: AccountRepository,
) : FinishImportLedgerInteractor {
override suspend fun createWallet(name: String, ledgerChainAccounts: Map<ChainId, LedgerSubstrateAccount>) = runCatching {
val addAccountResult = legacyLedgerAddAccountRepository.addAccountWithSingleChange(
LegacyLedgerAddAccountRepository.Payload.MetaAccount(
name,
ledgerChainAccounts
)
)
accountRepository.selectMetaAccount(addAccountResult.metaId)
}
}
@@ -0,0 +1,61 @@
package io.novafoundation.nova.feature_ledger_impl.domain.account.sign
import io.novafoundation.nova.common.utils.chainId
import io.novafoundation.nova.feature_account_api.data.signer.SeparateFlowSignerState
import io.novafoundation.nova.feature_account_api.data.signer.chainId
import io.novafoundation.nova.feature_account_api.data.signer.signaturePayload
import io.novafoundation.nova.feature_account_api.domain.model.LedgerVariant
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_impl.domain.migration.LedgerMigrationUseCase
import io.novafoundation.nova.runtime.ext.verifyMultiChain
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novasama.substrate_sdk_android.encrypt.SignatureVerifier
import io.novasama.substrate_sdk_android.encrypt.SignatureWrapper
import io.novasama.substrate_sdk_android.runtime.extrinsic.v5.transactionExtension.InheritedImplication
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
interface SignLedgerInteractor {
suspend fun getSignature(
device: LedgerDevice,
metaId: Long,
payload: InheritedImplication,
): SignatureWrapper
suspend fun verifySignature(
payload: SeparateFlowSignerState,
signature: SignatureWrapper
): Boolean
}
class RealSignLedgerInteractor(
private val chainRegistry: ChainRegistry,
private val usedVariant: LedgerVariant,
private val migrationUseCase: LedgerMigrationUseCase
) : SignLedgerInteractor {
override suspend fun getSignature(
device: LedgerDevice,
metaId: Long,
payload: InheritedImplication
): SignatureWrapper = withContext(Dispatchers.Default) {
val chainId = payload.chainId
val app = migrationUseCase.determineLedgerApp(chainId, usedVariant)
app.getSignature(device, metaId, chainId, payload)
}
override suspend fun verifySignature(
payload: SeparateFlowSignerState,
signature: SignatureWrapper
): Boolean = runCatching {
val payloadBytes = payload.payload.signaturePayload()
val chainId = payload.payload.chainId()
val chain = chainRegistry.getChain(chainId)
val publicKey = payload.metaAccount.publicKeyIn(chain) ?: throw IllegalStateException("No public key for chain $chainId")
SignatureVerifier.verifyMultiChain(chain, signature, payloadBytes, publicKey)
}.getOrDefault(false)
}
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_ledger_impl.domain.migration
import io.novafoundation.nova.feature_account_api.domain.model.LedgerVariant
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.SubstrateLedgerApplication
import io.novafoundation.nova.feature_ledger_core.domain.LedgerMigrationTracker
import io.novafoundation.nova.feature_ledger_impl.sdk.application.substrate.legacyApp.LegacySubstrateLedgerApplication
import io.novafoundation.nova.feature_ledger_impl.sdk.application.substrate.newApp.GenericSubstrateLedgerApplication
import io.novafoundation.nova.feature_ledger_impl.sdk.application.substrate.newApp.MigrationSubstrateLedgerApplication
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
interface LedgerMigrationUseCase {
suspend fun determineLedgerApp(chainId: ChainId, ledgerVariant: LedgerVariant): SubstrateLedgerApplication
}
suspend fun LedgerMigrationUseCase.determineAppForLegacyAccount(chainId: ChainId): SubstrateLedgerApplication {
return determineLedgerApp(chainId, LedgerVariant.LEGACY)
}
class RealLedgerMigrationUseCase(
private val ledgerMigrationTracker: LedgerMigrationTracker,
private val migrationApp: MigrationSubstrateLedgerApplication,
private val legacyApp: LegacySubstrateLedgerApplication,
private val genericApp: GenericSubstrateLedgerApplication,
) : LedgerMigrationUseCase {
override suspend fun determineLedgerApp(chainId: ChainId, ledgerVariant: LedgerVariant): SubstrateLedgerApplication {
return when {
ledgerVariant == LedgerVariant.GENERIC -> genericApp
ledgerMigrationTracker.shouldUseMigrationApp(chainId) -> migrationApp
else -> legacyApp
}
}
}
@@ -0,0 +1,42 @@
package io.novafoundation.nova.feature_ledger_impl.presentation
import io.novafoundation.nova.common.navigation.ReturnableRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress.AddEvmGenericLedgerAccountSelectAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectAddress.AddLedgerChainAccountSelectAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectLedgerAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.finish.FinishImportGenericLedgerPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.preview.PreviewImportGenericLedgerPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectLedger.SelectLedgerGenericPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.legacy.fillWallet.FillWalletImportLedgerLegacyPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.legacy.finish.FinishImportLedgerPayload
interface LedgerRouter : ReturnableRouter {
fun openImportFillWallet(payload: FillWalletImportLedgerLegacyPayload)
fun returnToImportFillWallet()
fun openSelectImportAddress(payload: SelectLedgerAddressPayload)
fun openCreatePincode()
fun openMain()
fun openFinishImportLedger(payload: FinishImportLedgerPayload)
fun finishSignFlow()
fun openAddChainAccountSelectAddress(payload: AddLedgerChainAccountSelectAddressPayload)
// Generic app flows
fun openSelectLedgerGeneric(payload: SelectLedgerGenericPayload)
fun openSelectAddressGenericLedger(payload: SelectLedgerAddressPayload)
fun openPreviewLedgerAccountsGeneric(payload: PreviewImportGenericLedgerPayload)
fun openFinishImportLedgerGeneric(payload: FinishImportGenericLedgerPayload)
fun openAddGenericEvmAddressSelectAddress(payload: AddEvmGenericLedgerAccountSelectAddressPayload)
}
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress
import android.os.Bundle
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.makeGone
import io.novafoundation.nova.feature_ledger_api.di.LedgerFeatureApi
import io.novafoundation.nova.feature_ledger_impl.di.LedgerFeatureComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectAddressLedgerFragment
class AddEvmGenericLedgerAccountSelectAddressFragment : SelectAddressLedgerFragment<AddEvmGenericLedgerAccountSelectAddressViewModel>() {
companion object {
private const val PAYLOAD_KEY = "AddEvmGenericLedgerAccountSelectAddressFragment.Payload"
fun getBundle(payload: AddEvmGenericLedgerAccountSelectAddressPayload): Bundle {
return Bundle().apply {
putParcelable(PAYLOAD_KEY, payload)
}
}
}
override fun initViews() {
super.initViews()
binder.ledgerSelectAddressChain.makeGone()
}
override fun inject() {
FeatureUtils.getFeature<LedgerFeatureComponent>(requireContext(), LedgerFeatureApi::class.java)
.addEvmGenericLedgerAccountSelectAddressComponentFactory()
.create(this, argument(PAYLOAD_KEY))
.inject(this)
}
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class AddEvmGenericLedgerAccountSelectAddressPayload(
val metaId: Long,
val deviceId: String,
) : Parcelable
@@ -0,0 +1,74 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress
import io.novafoundation.nova.common.address.AddressIconGenerator
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.domain.model.LedgerVariant
import io.novafoundation.nova.feature_account_api.presenatation.addressActions.AddressActionsMixin
import io.novafoundation.nova.feature_ledger_impl.domain.account.addChain.generic.AddEvmAccountToGenericLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.LedgerAccount
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.SelectAddressLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.generic.GenericLedgerEvmAlertFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectAddressLedgerViewModel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectLedgerAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.model.AddressVerificationMode
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.launch
class AddEvmGenericLedgerAccountSelectAddressViewModel(
private val router: LedgerRouter,
private val payload: AddEvmGenericLedgerAccountSelectAddressPayload,
private val addAccountInteractor: AddEvmAccountToGenericLedgerInteractor,
private val selectAddressLedgerInteractor: SelectAddressLedgerInteractor,
private val evmAlertFormatter: GenericLedgerEvmAlertFormatter,
addressIconGenerator: AddressIconGenerator,
resourceManager: ResourceManager,
chainRegistry: ChainRegistry,
selectLedgerAddressPayload: SelectLedgerAddressPayload,
messageCommandFormatter: MessageCommandFormatter,
addressActionsMixinFactory: AddressActionsMixin.Factory
) : SelectAddressLedgerViewModel(
router = router,
interactor = selectAddressLedgerInteractor,
addressIconGenerator = addressIconGenerator,
resourceManager = resourceManager,
payload = selectLedgerAddressPayload,
chainRegistry = chainRegistry,
messageCommandFormatter = messageCommandFormatter,
addressActionsMixinFactory = addressActionsMixinFactory
) {
override val ledgerVariant: LedgerVariant = LedgerVariant.GENERIC
override val addressVerificationMode = AddressVerificationMode.Enabled(addressSchemesToVerify = listOf(AddressScheme.EVM))
override suspend fun loadLedgerAccount(
substratePreviewChain: Chain,
deviceId: String,
accountIndex: Int,
ledgerVariant: LedgerVariant
): Result<LedgerAccount?> {
return selectAddressLedgerInteractor.loadLedgerAccount(substratePreviewChain, deviceId, accountIndex, ledgerVariant).map { ledgerAccount ->
if (ledgerAccount.evm != null) {
ledgerAccount
} else {
_alertFlow.emit(evmAlertFormatter.createUpdateAppToGetEvmAddressAlert())
null
}
}
}
override fun onAccountVerified(account: LedgerAccount) {
launch {
val result = addAccountInteractor.addEvmAccount(payload.metaId, account.evm!!)
result
.onSuccess { router.openMain() }
.onFailure(::showError)
}
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress.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_ledger_impl.presentation.account.addChain.generic.selectAddress.AddEvmGenericLedgerAccountSelectAddressFragment
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress.AddEvmGenericLedgerAccountSelectAddressPayload
@Subcomponent(
modules = [
AddEvmGenericLedgerAccountSelectAddressModule::class
]
)
@ScreenScope
interface AddEvmGenericLedgerAccountSelectAddressComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance payload: AddEvmGenericLedgerAccountSelectAddressPayload,
): AddEvmGenericLedgerAccountSelectAddressComponent
}
fun inject(fragment: AddEvmGenericLedgerAccountSelectAddressFragment)
}
@@ -0,0 +1,76 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress.di
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.address.AddressIconGenerator
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.feature_account_api.presenatation.addressActions.AddressActionsMixin
import io.novafoundation.nova.feature_ledger_impl.di.annotations.GenericLedger
import io.novafoundation.nova.feature_ledger_impl.domain.account.addChain.generic.AddEvmAccountToGenericLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.domain.account.addChain.generic.RealAddEvmAccountToGenericLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.SelectAddressLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress.AddEvmGenericLedgerAccountSelectAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress.AddEvmGenericLedgerAccountSelectAddressViewModel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress.di.AddEvmGenericLedgerAccountSelectAddressModule.BindsModule
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.generic.GenericLedgerEvmAlertFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectLedgerAddressPayload
import io.novafoundation.nova.runtime.ext.Geneses
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
@Module(includes = [ViewModelModule::class, BindsModule::class])
class AddEvmGenericLedgerAccountSelectAddressModule {
@Module
interface BindsModule {
@Binds
fun bindInteractor(real: RealAddEvmAccountToGenericLedgerInteractor): AddEvmAccountToGenericLedgerInteractor
}
@Provides
@IntoMap
@ViewModelKey(AddEvmGenericLedgerAccountSelectAddressViewModel::class)
fun provideViewModel(
router: LedgerRouter,
payload: AddEvmGenericLedgerAccountSelectAddressPayload,
addAccountInteractor: AddEvmAccountToGenericLedgerInteractor,
selectAddressLedgerInteractor: SelectAddressLedgerInteractor,
addressIconGenerator: AddressIconGenerator,
resourceManager: ResourceManager,
chainRegistry: ChainRegistry,
@GenericLedger messageCommandFormatter: MessageCommandFormatter,
evmAlertFormatter: GenericLedgerEvmAlertFormatter,
addressActionsMixinFactory: AddressActionsMixin.Factory
): ViewModel {
val selectLedgerAddressPayload = SelectLedgerAddressPayload(payload.deviceId, substrateChainId = Chain.Geneses.POLKADOT)
return AddEvmGenericLedgerAccountSelectAddressViewModel(
router = router,
payload = payload,
selectAddressLedgerInteractor = selectAddressLedgerInteractor,
addressIconGenerator = addressIconGenerator,
resourceManager = resourceManager,
chainRegistry = chainRegistry,
selectLedgerAddressPayload = selectLedgerAddressPayload,
messageCommandFormatter = messageCommandFormatter,
addAccountInteractor = addAccountInteractor,
evmAlertFormatter = evmAlertFormatter,
addressActionsMixinFactory = addressActionsMixinFactory
)
}
@Provides
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): AddEvmGenericLedgerAccountSelectAddressViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(AddEvmGenericLedgerAccountSelectAddressViewModel::class.java)
}
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectLedger
import android.os.Bundle
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.feature_ledger_api.di.LedgerFeatureApi
import io.novafoundation.nova.feature_ledger_impl.di.LedgerFeatureComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerFragment
class AddEvmAccountSelectGenericLedgerFragment : SelectLedgerFragment<AddEvmAccountSelectGenericLedgerViewModel>() {
companion object {
private const val KEY_ADD_ACCOUNT_PAYLOAD = "AddEvmAccountSelectGenericLedgerFragment.Payload"
fun getBundle(payload: AddEvmAccountSelectGenericLedgerPayload): Bundle {
return Bundle().apply {
putParcelable(KEY_ADD_ACCOUNT_PAYLOAD, payload)
}
}
}
override fun inject() {
FeatureUtils.getFeature<LedgerFeatureComponent>(requireContext(), LedgerFeatureApi::class.java)
.addEvmAccountSelectGenericLedgerComponentFactory()
.create(this, argument(KEY_ADD_ACCOUNT_PAYLOAD))
.inject(this)
}
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectLedger
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerPayload
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
class AddEvmAccountSelectGenericLedgerPayload(val metaId: Long) : SelectLedgerPayload {
@IgnoredOnParcel
override val connectionMode: SelectLedgerPayload.ConnectionMode = SelectLedgerPayload.ConnectionMode.ALL
}
@@ -0,0 +1,47 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectLedger
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.bluetooth.BluetoothManager
import io.novafoundation.nova.common.utils.event
import io.novafoundation.nova.common.utils.location.LocationManager
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectAddress.AddEvmGenericLedgerAccountSelectAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers.LedgerDeviceFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerViewModel
class AddEvmAccountSelectGenericLedgerViewModel(
private val router: LedgerRouter,
private val payload: AddEvmAccountSelectGenericLedgerPayload,
private val messageCommandFormatter: MessageCommandFormatter,
discoveryService: LedgerDeviceDiscoveryService,
permissionsAsker: PermissionsAsker.Presentation,
bluetoothManager: BluetoothManager,
locationManager: LocationManager,
resourceManager: ResourceManager,
messageFormatter: LedgerMessageFormatter,
ledgerDeviceFormatter: LedgerDeviceFormatter
) : SelectLedgerViewModel(
discoveryService = discoveryService,
permissionsAsker = permissionsAsker,
bluetoothManager = bluetoothManager,
locationManager = locationManager,
router = router,
resourceManager = resourceManager,
messageFormatter = messageFormatter,
ledgerDeviceFormatter = ledgerDeviceFormatter,
messageCommandFormatter = messageCommandFormatter,
payload = payload
) {
override suspend fun verifyConnection(device: LedgerDevice) {
ledgerMessageCommands.value = messageCommandFormatter.hideCommand().event()
val payload = AddEvmGenericLedgerAccountSelectAddressPayload(payload.metaId, device.id)
router.openAddGenericEvmAddressSelectAddress(payload)
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectLedger.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_ledger_impl.presentation.account.addChain.generic.selectLedger.AddEvmAccountSelectGenericLedgerFragment
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectLedger.AddEvmAccountSelectGenericLedgerPayload
@Subcomponent(
modules = [
AddEvmAccountSelectGenericLedgerModule::class
]
)
@ScreenScope
interface AddEvmAccountSelectGenericLedgerComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance payload: AddEvmAccountSelectGenericLedgerPayload,
): AddEvmAccountSelectGenericLedgerComponent
}
fun inject(fragment: AddEvmAccountSelectGenericLedgerFragment)
}
@@ -0,0 +1,61 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectLedger.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.modules.shared.PermissionAskerForFragmentModule
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.bluetooth.BluetoothManager
import io.novafoundation.nova.common.utils.location.LocationManager
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_impl.di.annotations.GenericLedger
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectLedger.AddEvmAccountSelectGenericLedgerPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.generic.selectLedger.AddEvmAccountSelectGenericLedgerViewModel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers.LedgerDeviceFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatter
@Module(includes = [ViewModelModule::class, PermissionAskerForFragmentModule::class])
class AddEvmAccountSelectGenericLedgerModule {
@Provides
@IntoMap
@ViewModelKey(AddEvmAccountSelectGenericLedgerViewModel::class)
fun provideViewModel(
payload: AddEvmAccountSelectGenericLedgerPayload,
discoveryService: LedgerDeviceDiscoveryService,
permissionsAsker: PermissionsAsker.Presentation,
bluetoothManager: BluetoothManager,
locationManager: LocationManager,
router: LedgerRouter,
resourceManager: ResourceManager,
ledgerDeviceFormatter: LedgerDeviceFormatter,
@GenericLedger messageFormatter: LedgerMessageFormatter,
@GenericLedger messageCommandFormatter: MessageCommandFormatter
): ViewModel {
return AddEvmAccountSelectGenericLedgerViewModel(
discoveryService = discoveryService,
permissionsAsker = permissionsAsker,
bluetoothManager = bluetoothManager,
locationManager = locationManager,
router = router,
resourceManager = resourceManager,
payload = payload,
messageFormatter = messageFormatter,
ledgerDeviceFormatter = ledgerDeviceFormatter,
messageCommandFormatter = messageCommandFormatter
)
}
@Provides
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): AddEvmAccountSelectGenericLedgerViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(AddEvmAccountSelectGenericLedgerViewModel::class.java)
}
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectAddress
import android.os.Bundle
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.feature_ledger_api.di.LedgerFeatureApi
import io.novafoundation.nova.feature_ledger_impl.di.LedgerFeatureComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectAddressLedgerFragment
class AddLedgerChainAccountSelectAddressFragment : SelectAddressLedgerFragment<AddLedgerChainAccountSelectAddressViewModel>() {
companion object {
private const val PAYLOAD_KEY = "AddChainAccountSelectAddressLedgerFragment.Payload"
fun getBundle(payload: AddLedgerChainAccountSelectAddressPayload): Bundle {
return Bundle().apply {
putParcelable(PAYLOAD_KEY, payload)
}
}
}
override fun inject() {
FeatureUtils.getFeature<LedgerFeatureComponent>(requireContext(), LedgerFeatureApi::class.java)
.addChainAccountSelectAddressComponentFactory()
.create(this, argument(PAYLOAD_KEY))
.inject(this)
}
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectAddress
import android.os.Parcelable
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import kotlinx.parcelize.Parcelize
@Parcelize
class AddLedgerChainAccountSelectAddressPayload(
val chainId: ChainId,
val metaId: Long,
val deviceId: String,
) : Parcelable
@@ -0,0 +1,58 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectAddress
import io.novafoundation.nova.common.address.AddressIconGenerator
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.domain.model.LedgerVariant
import io.novafoundation.nova.feature_account_api.presenatation.addressActions.AddressActionsMixin
import io.novafoundation.nova.feature_ledger_impl.domain.account.addChain.legacy.AddLedgerChainAccountInteractor
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.LedgerAccount
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.SelectAddressLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectAddressLedgerViewModel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectLedgerAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.model.AddressVerificationMode
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AddLedgerChainAccountSelectAddressViewModel(
private val router: LedgerRouter,
private val payload: AddLedgerChainAccountSelectAddressPayload,
private val addChainAccountInteractor: AddLedgerChainAccountInteractor,
selectAddressLedgerInteractor: SelectAddressLedgerInteractor,
addressIconGenerator: AddressIconGenerator,
resourceManager: ResourceManager,
chainRegistry: ChainRegistry,
selectLedgerAddressPayload: SelectLedgerAddressPayload,
messageCommandFormatter: MessageCommandFormatter,
addressActionsMixinFactory: AddressActionsMixin.Factory
) : SelectAddressLedgerViewModel(
router = router,
interactor = selectAddressLedgerInteractor,
addressIconGenerator = addressIconGenerator,
resourceManager = resourceManager,
payload = selectLedgerAddressPayload,
chainRegistry = chainRegistry,
messageCommandFormatter = messageCommandFormatter,
addressActionsMixinFactory = addressActionsMixinFactory
) {
override val ledgerVariant: LedgerVariant = LedgerVariant.LEGACY
override val addressVerificationMode = AddressVerificationMode.Enabled(addressSchemesToVerify = listOf(AddressScheme.SUBSTRATE))
override fun onAccountVerified(account: LedgerAccount) {
launch {
val result = withContext(Dispatchers.Default) {
addChainAccountInteractor.addChainAccount(payload.metaId, payload.chainId, account.substrate)
}
result.onSuccess {
router.openMain()
}.onFailure(::showError)
}
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectAddress.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_ledger_impl.presentation.account.addChain.legacy.selectAddress.AddLedgerChainAccountSelectAddressFragment
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectAddress.AddLedgerChainAccountSelectAddressPayload
@Subcomponent(
modules = [
AddLedgerChainAccountSelectAddressModule::class
]
)
@ScreenScope
interface AddLedgerChainAccountSelectAddressComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance payload: AddLedgerChainAccountSelectAddressPayload,
): AddLedgerChainAccountSelectAddressComponent
}
fun inject(fragment: AddLedgerChainAccountSelectAddressFragment)
}
@@ -0,0 +1,96 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectAddress.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.address.AddressIconGenerator
import io.novafoundation.nova.common.di.scope.ScreenScope
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.feature_account_api.data.repository.addAccount.ledger.LegacyLedgerAddAccountRepository
import io.novafoundation.nova.feature_account_api.presenatation.addressActions.AddressActionsMixin
import io.novafoundation.nova.feature_ledger_impl.domain.account.addChain.legacy.AddLedgerChainAccountInteractor
import io.novafoundation.nova.feature_ledger_impl.domain.account.addChain.legacy.RealAddLedgerChainAccountInteractor
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.SelectAddressLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectAddress.AddLedgerChainAccountSelectAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectAddress.AddLedgerChainAccountSelectAddressViewModel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatterFactory
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatterFactory
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectLedgerAddressPayload
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
@Module(includes = [ViewModelModule::class])
class AddLedgerChainAccountSelectAddressModule {
@Provides
@ScreenScope
fun provideInteractor(
legacyLedgerAddAccountRepository: LegacyLedgerAddAccountRepository
): AddLedgerChainAccountInteractor = RealAddLedgerChainAccountInteractor(legacyLedgerAddAccountRepository)
@Provides
@ScreenScope
fun provideSelectLedgerAddressPayload(
screenPayload: AddLedgerChainAccountSelectAddressPayload
): SelectLedgerAddressPayload = SelectLedgerAddressPayload(
deviceId = screenPayload.deviceId,
substrateChainId = screenPayload.chainId
)
@Provides
@ScreenScope
fun provideMessageFormatter(
screenPayload: AddLedgerChainAccountSelectAddressPayload,
factory: LedgerMessageFormatterFactory,
): LedgerMessageFormatter = factory.createLegacy(screenPayload.chainId, showAlerts = false)
@Provides
@ScreenScope
fun provideMessageCommandFormatter(
messageFormatter: LedgerMessageFormatter,
messageCommandFormatterFactory: MessageCommandFormatterFactory
): MessageCommandFormatter = messageCommandFormatterFactory.create(messageFormatter)
@Provides
@IntoMap
@ViewModelKey(AddLedgerChainAccountSelectAddressViewModel::class)
fun provideViewModel(
router: LedgerRouter,
payload: AddLedgerChainAccountSelectAddressPayload,
addChainAccountInteractor: AddLedgerChainAccountInteractor,
selectAddressLedgerInteractor: SelectAddressLedgerInteractor,
addressIconGenerator: AddressIconGenerator,
resourceManager: ResourceManager,
chainRegistry: ChainRegistry,
selectLedgerAddressPayload: SelectLedgerAddressPayload,
messageCommandFormatter: MessageCommandFormatter,
addressActionsMixinFactory: AddressActionsMixin.Factory
): ViewModel {
return AddLedgerChainAccountSelectAddressViewModel(
router = router,
payload = payload,
addChainAccountInteractor = addChainAccountInteractor,
selectAddressLedgerInteractor = selectAddressLedgerInteractor,
addressIconGenerator = addressIconGenerator,
resourceManager = resourceManager,
chainRegistry = chainRegistry,
selectLedgerAddressPayload = selectLedgerAddressPayload,
messageCommandFormatter = messageCommandFormatter,
addressActionsMixinFactory = addressActionsMixinFactory
)
}
@Provides
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): AddLedgerChainAccountSelectAddressViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(
AddLedgerChainAccountSelectAddressViewModel::class.java
)
}
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectLedger
import android.os.Bundle
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.feature_ledger_api.di.LedgerFeatureApi
import io.novafoundation.nova.feature_ledger_impl.di.LedgerFeatureComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerFragment
class AddChainAccountSelectLedgerFragment : SelectLedgerFragment<AddChainAccountSelectLedgerViewModel>() {
companion object {
private const val KEY_ADD_ACCOUNT_PAYLOAD = "AddChainAccountSelectLedgerFragment.Payload"
fun getBundle(payload: AddChainAccountSelectLedgerPayload): Bundle {
return Bundle().apply {
putParcelable(KEY_ADD_ACCOUNT_PAYLOAD, payload)
}
}
}
override fun inject() {
FeatureUtils.getFeature<LedgerFeatureComponent>(requireContext(), LedgerFeatureApi::class.java)
.addChainAccountSelectLedgerComponentFactory()
.create(this, argument(KEY_ADD_ACCOUNT_PAYLOAD))
.inject(this)
}
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectLedger
import io.novafoundation.nova.feature_account_api.presenatation.account.add.AddAccountPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerPayload
import kotlinx.android.parcel.Parcelize
@Parcelize
class AddChainAccountSelectLedgerPayload(
val addAccountPayload: AddAccountPayload.ChainAccount,
override val connectionMode: SelectLedgerPayload.ConnectionMode
) : SelectLedgerPayload
@@ -0,0 +1,57 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectLedger
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.bluetooth.BluetoothManager
import io.novafoundation.nova.common.utils.event
import io.novafoundation.nova.common.utils.location.LocationManager
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_impl.domain.migration.LedgerMigrationUseCase
import io.novafoundation.nova.feature_ledger_impl.domain.migration.determineAppForLegacyAccount
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectAddress.AddLedgerChainAccountSelectAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers.LedgerDeviceFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerViewModel
class AddChainAccountSelectLedgerViewModel(
private val migrationUseCase: LedgerMigrationUseCase,
private val router: LedgerRouter,
private val payload: AddChainAccountSelectLedgerPayload,
private val messageCommandFormatter: MessageCommandFormatter,
discoveryService: LedgerDeviceDiscoveryService,
permissionsAsker: PermissionsAsker.Presentation,
bluetoothManager: BluetoothManager,
locationManager: LocationManager,
resourceManager: ResourceManager,
messageFormatter: LedgerMessageFormatter,
ledgerDeviceFormatter: LedgerDeviceFormatter
) : SelectLedgerViewModel(
discoveryService = discoveryService,
permissionsAsker = permissionsAsker,
bluetoothManager = bluetoothManager,
locationManager = locationManager,
router = router,
resourceManager = resourceManager,
messageFormatter = messageFormatter,
ledgerDeviceFormatter = ledgerDeviceFormatter,
messageCommandFormatter = messageCommandFormatter,
payload = payload
) {
private val addAccountPayload = payload.addAccountPayload
override suspend fun verifyConnection(device: LedgerDevice) {
ledgerMessageCommands.value = messageCommandFormatter.hideCommand().event()
val app = migrationUseCase.determineAppForLegacyAccount(addAccountPayload.chainId)
// ensure that address loads successfully
app.getSubstrateAccount(device, addAccountPayload.chainId, accountIndex = 0, confirmAddress = false)
val payload = AddLedgerChainAccountSelectAddressPayload(addAccountPayload.chainId, addAccountPayload.metaId, device.id)
router.openAddChainAccountSelectAddress(payload)
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectLedger.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_ledger_impl.presentation.account.addChain.legacy.selectLedger.AddChainAccountSelectLedgerPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectLedger.AddChainAccountSelectLedgerFragment
@Subcomponent(
modules = [
AddChainAccountSelectLedgerModule::class
]
)
@ScreenScope
interface AddChainAccountSelectLedgerComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance payload: AddChainAccountSelectLedgerPayload,
): AddChainAccountSelectLedgerComponent
}
fun inject(fragment: AddChainAccountSelectLedgerFragment)
}
@@ -0,0 +1,82 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectLedger.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.modules.shared.PermissionAskerForFragmentModule
import io.novafoundation.nova.common.di.scope.ScreenScope
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.bluetooth.BluetoothManager
import io.novafoundation.nova.common.utils.location.LocationManager
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_impl.domain.migration.LedgerMigrationUseCase
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectLedger.AddChainAccountSelectLedgerPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.addChain.legacy.selectLedger.AddChainAccountSelectLedgerViewModel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatterFactory
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers.LedgerDeviceFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatterFactory
@Module(includes = [ViewModelModule::class, PermissionAskerForFragmentModule::class])
class AddChainAccountSelectLedgerModule {
@Provides
@ScreenScope
fun provideMessageFormatter(
payload: AddChainAccountSelectLedgerPayload,
factory: LedgerMessageFormatterFactory,
): LedgerMessageFormatter = factory.createLegacy(payload.addAccountPayload.chainId, showAlerts = false)
@Provides
@ScreenScope
fun provideMessageCommandFormatter(
messageFormatter: LedgerMessageFormatter,
messageCommandFormatterFactory: MessageCommandFormatterFactory
): MessageCommandFormatter = messageCommandFormatterFactory.create(messageFormatter)
@Provides
@IntoMap
@ViewModelKey(AddChainAccountSelectLedgerViewModel::class)
fun provideViewModel(
migrationUseCase: LedgerMigrationUseCase,
payload: AddChainAccountSelectLedgerPayload,
discoveryService: LedgerDeviceDiscoveryService,
permissionsAsker: PermissionsAsker.Presentation,
bluetoothManager: BluetoothManager,
locationManager: LocationManager,
router: LedgerRouter,
resourceManager: ResourceManager,
messageFormatter: LedgerMessageFormatter,
ledgerDeviceFormatter: LedgerDeviceFormatter,
messageCommandFormatter: MessageCommandFormatter
): ViewModel {
return AddChainAccountSelectLedgerViewModel(
migrationUseCase = migrationUseCase,
discoveryService = discoveryService,
permissionsAsker = permissionsAsker,
bluetoothManager = bluetoothManager,
locationManager = locationManager,
router = router,
resourceManager = resourceManager,
payload = payload,
messageFormatter = messageFormatter,
ledgerDeviceFormatter = ledgerDeviceFormatter,
messageCommandFormatter = messageCommandFormatter
)
}
@Provides
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): AddChainAccountSelectLedgerViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(
AddChainAccountSelectLedgerViewModel::class.java
)
}
}
@@ -0,0 +1,182 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import io.novafoundation.nova.common.utils.formatting.TimerValue
import io.novafoundation.nova.common.utils.setTextColorRes
import io.novafoundation.nova.common.utils.setVisible
import io.novafoundation.nova.common.view.AlertModel
import io.novafoundation.nova.common.view.bottomSheet.BaseBottomSheet
import io.novafoundation.nova.common.view.setModelOrHide
import io.novafoundation.nova.common.view.startTimer
import io.novafoundation.nova.common.view.stopTimer
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.databinding.FragmentLedgerMessageBinding
sealed class LedgerMessageCommand {
companion object
object Hide : LedgerMessageCommand()
sealed class Show(
val title: String,
val subtitle: String,
val graphics: Graphics,
val alert: AlertModel?,
val onCancel: () -> Unit,
) : LedgerMessageCommand() {
sealed class Error(
title: String,
subtitle: String,
graphics: Graphics,
alert: AlertModel?,
onCancel: () -> Unit,
) : Show(title, subtitle, graphics, alert, onCancel) {
class RecoverableError(
title: String,
subtitle: String,
graphics: Graphics,
alert: AlertModel?,
onCancel: () -> Unit,
val onRetry: () -> Unit
) : Error(title, subtitle, graphics, alert, onCancel)
class FatalError(
title: String,
subtitle: String,
graphics: Graphics,
alert: AlertModel?,
val onConfirm: () -> Unit,
onCancel: () -> Unit = onConfirm, // when error is fatal, confirm is the same as hide by default
) : Error(title, subtitle, graphics, alert, onCancel)
}
class Info(
title: String,
subtitle: String,
graphics: Graphics,
onCancel: () -> Unit,
alert: AlertModel?,
val footer: Footer
) : Show(title, subtitle, graphics, alert, onCancel)
}
sealed class Footer {
class Timer(
val timerValue: TimerValue,
val closeToExpire: (TimerValue) -> Boolean,
val timerFinished: () -> Unit,
@StringRes val messageFormat: Int
) : Footer()
class Value(val value: String) : Footer()
class Rows(
val first: Column,
val second: Column
) : Footer() {
class Column(
val label: String,
val value: String
)
}
}
class Graphics(@DrawableRes val ledgerImageRes: Int)
}
class LedgerMessageBottomSheet(
context: Context,
) : BaseBottomSheet<FragmentLedgerMessageBinding>(context) {
override val binder = FragmentLedgerMessageBinding.inflate(LayoutInflater.from(context))
val container: View
get() = binder.ledgerMessageContainer
fun receiveCommand(command: LedgerMessageCommand) {
binder.ledgerMessageActions.setVisible(command is LedgerMessageCommand.Show.Error)
binder.ledgerMessageCancel.setVisible(command is LedgerMessageCommand.Show.Error.RecoverableError)
setupFooterVisibility(command is LedgerMessageCommand.Show.Info)
when (command) {
LedgerMessageCommand.Hide -> dismiss()
is LedgerMessageCommand.Show.Error.FatalError -> {
setupBaseShow(command)
binder.ledgerMessageConfirm.setOnClickListener { command.onConfirm() }
binder.ledgerMessageConfirm.setText(R.string.common_ok_back)
}
is LedgerMessageCommand.Show.Error.RecoverableError -> {
setupBaseShow(command)
binder.ledgerMessageConfirm.setOnClickListener { command.onRetry() }
binder.ledgerMessageConfirm.setText(R.string.common_retry)
binder.ledgerMessageCancel.setOnClickListener { command.onCancel() }
}
is LedgerMessageCommand.Show.Info -> {
setupBaseShow(command)
showFooter(command.footer)
}
}
}
private fun setupFooterVisibility(visible: Boolean) {
binder.ledgerMessageFooterMessage.setVisible(visible)
binder.ledgerMessageFooterColumns.setVisible(visible)
if (!visible) {
binder.ledgerMessageFooterMessage.stopTimer()
}
}
private fun showFooter(footer: LedgerMessageCommand.Footer) {
binder.ledgerMessageFooterMessage.setVisible(footer !is LedgerMessageCommand.Footer.Rows)
binder.ledgerMessageFooterColumns.setVisible(footer is LedgerMessageCommand.Footer.Rows)
when (footer) {
is LedgerMessageCommand.Footer.Value -> {
binder.ledgerMessageFooterMessage.text = footer.value
}
is LedgerMessageCommand.Footer.Timer -> {
binder.ledgerMessageFooterMessage.startTimer(
value = footer.timerValue,
customMessageFormat = footer.messageFormat,
onTick = { view, _ ->
val textColorRes = if (footer.closeToExpire(footer.timerValue)) R.color.text_negative else R.color.text_secondary
view.setTextColorRes(textColorRes)
},
onFinish = { footer.timerFinished() }
)
}
is LedgerMessageCommand.Footer.Rows -> {
binder.ledgerMessageFooterTitle1.text = footer.first.label
binder.ledgerMessageFooterMessage1.text = footer.first.value
binder.ledgerMessageFooterTitle2.text = footer.second.label
binder.ledgerMessageFooterMessage2.text = footer.second.value
}
}
}
private fun setupBaseShow(command: LedgerMessageCommand.Show) {
binder.ledgerMessageTitle.text = command.title
binder.ledgerMessageSubtitle.text = command.subtitle
binder.ledgerMessageImage.setImageResource(command.graphics.ledgerImageRes)
binder.ledgerMessageAlert.setModelOrHide(command.alert)
setOnCancelListener { command.onCancel() }
}
}
@@ -0,0 +1,62 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet
import android.content.Context
import android.view.View
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.utils.Event
interface LedgerMessagePresentable {
fun presentCommand(command: LedgerMessageCommand, context: Context)
}
interface LedgerMessageCommands {
val ledgerMessageCommands: MutableLiveData<Event<LedgerMessageCommand>>
}
class SingleSheetLedgerMessagePresentable : LedgerMessagePresentable {
private var bottomSheet: LedgerMessageBottomSheet? = null
override fun presentCommand(command: LedgerMessageCommand, context: Context) {
when {
bottomSheet == null && command is LedgerMessageCommand.Show -> {
bottomSheet = LedgerMessageBottomSheet(context)
bottomSheet?.receiveCommand(command)
bottomSheet?.show()
}
bottomSheet != null && command is LedgerMessageCommand.Show -> {
bottomSheet?.container?.stateChangeTransition {
bottomSheet?.receiveCommand(command)
}
}
else -> {
bottomSheet?.receiveCommand(command)
bottomSheet = null
}
}
}
private fun View.stateChangeTransition(onChangeState: () -> Unit) {
animate()
.alpha(0f)
.withEndAction {
onChangeState()
animate()
.alpha(1f)
.start()
}.start()
}
}
fun <F, V> F.setupLedgerMessages(presentable: LedgerMessagePresentable)
where F : BaseFragment<V, *>, V : LedgerMessageCommands {
viewModel.ledgerMessageCommands.observeEvent {
presentable.presentCommand(it, requireContext())
}
}
@@ -0,0 +1,215 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.common.address.format.AddressSchemeFormatter
import io.novafoundation.nova.common.mixin.api.Browserable
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.second
import io.novafoundation.nova.common.view.AlertModel
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerApplicationResponse
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand.Footer.Rows.Column
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand.Show.Error.RecoverableError
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers.LedgerDeviceFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatter.MessageKind
import io.novafoundation.nova.runtime.extrinsic.ValidityPeriod
import io.novafoundation.nova.runtime.extrinsic.closeToExpire
class MessageCommandFormatterFactory(
private val resourceManager: ResourceManager,
private val deviceMapper: LedgerDeviceFormatter,
private val addressSchemeFormatter: AddressSchemeFormatter
) {
fun create(messageFormatter: LedgerMessageFormatter): MessageCommandFormatter {
return MessageCommandFormatter(resourceManager, deviceMapper, messageFormatter, addressSchemeFormatter)
}
}
class MessageCommandFormatter(
private val resourceManager: ResourceManager,
private val deviceMapper: LedgerDeviceFormatter,
private val messageFormatter: LedgerMessageFormatter,
private val addressSchemeFormatter: AddressSchemeFormatter,
) {
context(Browserable.Presentation)
suspend fun unknownError(
device: LedgerDevice,
onRetry: () -> Unit,
onCancel: () -> Unit
): LedgerMessageCommand {
return retryCommand(
title = resourceManager.getString(R.string.ledger_error_general_title),
subtitle = resourceManager.getString(R.string.ledger_error_general_message),
alertModel = messageFormatter.alertForKind(MessageKind.OTHER),
device = device,
onRetry = onRetry,
onCancel = onCancel
)
}
context(Browserable.Presentation)
suspend fun substrateApplicationError(
reason: LedgerApplicationResponse,
device: LedgerDevice,
onCancel: () -> Unit,
onRetry: () -> Unit
): LedgerMessageCommand {
val errorTitle: String
val errorMessage: String
val alert: AlertModel?
when (reason) {
LedgerApplicationResponse.APP_NOT_OPEN, LedgerApplicationResponse.WRONG_APP_OPEN -> {
val appName = messageFormatter.appName()
errorTitle = resourceManager.getString(R.string.ledger_error_app_not_launched_title, appName)
errorMessage = resourceManager.getString(R.string.ledger_error_app_not_launched_message, appName)
alert = messageFormatter.alertForKind(MessageKind.APP_NOT_OPEN)
}
LedgerApplicationResponse.TRANSACTION_REJECTED -> {
errorTitle = resourceManager.getString(R.string.ledger_error_app_cancelled_title)
errorMessage = resourceManager.getString(R.string.ledger_error_app_cancelled_message)
alert = messageFormatter.alertForKind(MessageKind.OTHER)
}
else -> {
errorTitle = resourceManager.getString(R.string.ledger_error_general_title)
errorMessage = resourceManager.getString(R.string.ledger_error_general_message)
alert = messageFormatter.alertForKind(MessageKind.OTHER)
}
}
return retryCommand(errorTitle, errorMessage, device, alert, onCancel, onRetry)
}
fun retryCommand(
title: String,
subtitle: String,
device: LedgerDevice,
alertModel: AlertModel?,
onCancel: () -> Unit,
onRetry: () -> Unit
): LedgerMessageCommand {
val deviceMapper = deviceMapper.createDelegate(device)
return RecoverableError(
title = title,
subtitle = subtitle,
alert = alertModel,
onCancel = onCancel,
onRetry = onRetry,
graphics = deviceMapper.getErrorImage()
)
}
context(Browserable.Presentation)
suspend fun fatalErrorCommand(
title: String,
subtitle: String,
device: LedgerDevice,
onCancel: () -> Unit,
onConfirm: () -> Unit,
): LedgerMessageCommand {
val deviceMapper = deviceMapper.createDelegate(device)
return LedgerMessageCommand.Show.Error.FatalError(
title = title,
subtitle = subtitle,
alert = messageFormatter.alertForKind(MessageKind.OTHER),
graphics = deviceMapper.getErrorImage(),
onCancel = onCancel,
onConfirm = onConfirm
)
}
context(Browserable.Presentation)
suspend fun signCommand(
validityPeriod: ValidityPeriod,
device: LedgerDevice,
onTimeFinished: () -> Unit,
onCancel: () -> Unit,
): LedgerMessageCommand {
val deviceMapper = deviceMapper.createDelegate(device)
return LedgerMessageCommand.Show.Info(
title = resourceManager.getString(R.string.ledger_review_approve_title),
subtitle = deviceMapper.getSignMessage(),
onCancel = onCancel,
alert = messageFormatter.alertForKind(MessageKind.OTHER),
graphics = deviceMapper.getSignImage(),
footer = LedgerMessageCommand.Footer.Timer(
timerValue = validityPeriod.period,
closeToExpire = { validityPeriod.closeToExpire() },
timerFinished = { onTimeFinished() },
messageFormat = R.string.ledger_sign_transaction_validity_format
)
)
}
fun reviewAddressCommand(
addresses: List<Pair<AddressScheme, String>>,
device: LedgerDevice,
onCancel: () -> Unit,
): LedgerMessageCommand {
val deviceMapper = deviceMapper.createDelegate(device)
val footer: LedgerMessageCommand.Footer
val subtitle: String
when (addresses.size) {
0 -> error("At least one address should be not null")
1 -> {
footer = LedgerMessageCommand.Footer.Value(
value = addresses.single().second.toTwoLinesAddress(),
)
subtitle = deviceMapper.getReviewAddressMessage()
}
2 -> {
footer = LedgerMessageCommand.Footer.Rows(
first = rowFor(addresses.first()),
second = rowFor(addresses.second())
)
subtitle = deviceMapper.getReviewAddressesMessage()
}
else -> error("Too many addresses passed: ${addresses.size}")
}
return LedgerMessageCommand.Show.Info(
title = resourceManager.getString(R.string.ledger_review_approve_title),
subtitle = subtitle,
onCancel = onCancel,
alert = null,
graphics = deviceMapper.getApproveImage(),
footer = footer
)
}
fun hideCommand(): LedgerMessageCommand {
return LedgerMessageCommand.Hide
}
private fun String.toTwoLinesAddress(): String {
val middle = length / 2
return substring(0, middle) + "\n" + substring(middle)
}
private fun rowFor(addressWithScheme: Pair<AddressScheme, String>): Column {
val label = addressSchemeFormatter.addressLabel(addressWithScheme.first)
return Column(label, addressWithScheme.second.toTwoLinesAddress())
}
}
@Suppress("UNCHECKED_CAST")
fun createLedgerReviewAddresses(
allowedAddressSchemes: List<AddressScheme>,
vararg allAddresses: Pair<AddressScheme, String?>
): List<Pair<AddressScheme, String>> {
return allAddresses.filter { it.first in allowedAddressSchemes && it.second != null } as List<Pair<AddressScheme, String>>
}
@@ -0,0 +1,39 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDeviceType
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand
class LedgerDeviceFormatter(private val resourceManager: ResourceManager) {
fun formatName(device: LedgerDevice): String = createDelegate(device).getName()
fun createDelegate(device: LedgerDevice): LedgerDeviceMapperDelegate {
return when (device.deviceType) {
LedgerDeviceType.STAX -> LedgerStaxMapperDelegate(resourceManager, device)
LedgerDeviceType.FLEX -> LedgerFlexMapperDelegate(resourceManager, device)
LedgerDeviceType.NANO_X -> LedgerNanoXUIMapperDelegate(resourceManager, device)
LedgerDeviceType.NANO_S_PLUS -> LedgerNanoSPlusMapperDelegate(resourceManager, device)
LedgerDeviceType.NANO_S -> LedgerNanoSMapperDelegate(resourceManager, device)
LedgerDeviceType.NANO_GEN5 -> LedgerNanoGen5UIMapperDelegate(resourceManager, device)
}
}
}
interface LedgerDeviceMapperDelegate {
fun getName(): String
fun getApproveImage(): LedgerMessageCommand.Graphics
fun getErrorImage(): LedgerMessageCommand.Graphics
fun getSignImage(): LedgerMessageCommand.Graphics
fun getReviewAddressMessage(): String
fun getReviewAddressesMessage(): String
fun getSignMessage(): String
}
@@ -0,0 +1,40 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand
class LedgerFlexMapperDelegate(
private val resourceManager: ResourceManager,
private val device: LedgerDevice
) : LedgerDeviceMapperDelegate {
override fun getName(): String {
return device.name ?: resourceManager.getString(R.string.ledger_device_flex)
}
override fun getApproveImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_flex_approve)
}
override fun getSignImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_flex_sign)
}
override fun getErrorImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_flex_error)
}
override fun getReviewAddressMessage(): String {
return resourceManager.getString(R.string.ledger_verify_address_message_confirm_button, getName())
}
override fun getReviewAddressesMessage(): String {
return resourceManager.getString(R.string.ledger_verify_addresses_message_confirm_button, getName())
}
override fun getSignMessage(): String {
return resourceManager.getString(R.string.ledger_hold_to_sign, getName())
}
}
@@ -0,0 +1,40 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand
class LedgerNanoGen5UIMapperDelegate(
private val resourceManager: ResourceManager,
private val device: LedgerDevice
) : LedgerDeviceMapperDelegate {
override fun getName(): String {
return device.name ?: resourceManager.getString(R.string.ledger_device_nano_gen5)
}
override fun getApproveImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_nano_gen5_approve)
}
override fun getSignImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_nano_gen5_sign)
}
override fun getErrorImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_nano_gen5_error)
}
override fun getReviewAddressMessage(): String {
return resourceManager.getString(R.string.ledger_verify_address_message_confirm_button, getName())
}
override fun getReviewAddressesMessage(): String {
return resourceManager.getString(R.string.ledger_verify_addresses_message_confirm_button, getName())
}
override fun getSignMessage(): String {
return resourceManager.getString(R.string.ledger_hold_to_sign, getName())
}
}
@@ -0,0 +1,40 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand
class LedgerNanoSMapperDelegate(
private val resourceManager: ResourceManager,
private val device: LedgerDevice
) : LedgerDeviceMapperDelegate {
override fun getName(): String {
return device.name ?: resourceManager.getString(R.string.ledger_device_nano_s)
}
override fun getApproveImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_nano_s_approve)
}
override fun getSignImage(): LedgerMessageCommand.Graphics {
return getApproveImage()
}
override fun getErrorImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_nano_s_error)
}
override fun getReviewAddressMessage(): String {
return resourceManager.getString(R.string.ledger_verify_address_message_both_buttons, getName())
}
override fun getReviewAddressesMessage(): String {
return resourceManager.getString(R.string.ledger_verify_addresses_message_both_buttons, getName())
}
override fun getSignMessage(): String {
return resourceManager.getString(R.string.ledger_sign_approve_message, getName())
}
}
@@ -0,0 +1,40 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand
class LedgerNanoSPlusMapperDelegate(
private val resourceManager: ResourceManager,
private val device: LedgerDevice
) : LedgerDeviceMapperDelegate {
override fun getName(): String {
return device.name ?: resourceManager.getString(R.string.ledger_device_nano_s_plus)
}
override fun getApproveImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_nano_s_approve)
}
override fun getSignImage(): LedgerMessageCommand.Graphics {
return getApproveImage()
}
override fun getErrorImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_nano_s_error)
}
override fun getReviewAddressMessage(): String {
return resourceManager.getString(R.string.ledger_verify_address_message_both_buttons, getName())
}
override fun getReviewAddressesMessage(): String {
return resourceManager.getString(R.string.ledger_verify_addresses_message_both_buttons, getName())
}
override fun getSignMessage(): String {
return resourceManager.getString(R.string.ledger_sign_approve_message, getName())
}
}
@@ -0,0 +1,40 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand
class LedgerNanoXUIMapperDelegate(
private val resourceManager: ResourceManager,
private val device: LedgerDevice
) : LedgerDeviceMapperDelegate {
override fun getName(): String {
return device.name ?: resourceManager.getString(R.string.ledger_device_nano_x)
}
override fun getApproveImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_nano_x_approve)
}
override fun getSignImage(): LedgerMessageCommand.Graphics {
return getApproveImage()
}
override fun getErrorImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_nano_x_error)
}
override fun getReviewAddressMessage(): String {
return resourceManager.getString(R.string.ledger_verify_address_message_both_buttons, getName())
}
override fun getReviewAddressesMessage(): String {
return resourceManager.getString(R.string.ledger_verify_addresses_message_both_buttons, getName())
}
override fun getSignMessage(): String {
return resourceManager.getString(R.string.ledger_sign_approve_message, getName())
}
}
@@ -0,0 +1,40 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand
class LedgerStaxMapperDelegate(
private val resourceManager: ResourceManager,
private val device: LedgerDevice
) : LedgerDeviceMapperDelegate {
override fun getName(): String {
return device.name ?: resourceManager.getString(R.string.ledger_device_stax)
}
override fun getApproveImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_stax_approve)
}
override fun getSignImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_stax_sign)
}
override fun getErrorImage(): LedgerMessageCommand.Graphics {
return LedgerMessageCommand.Graphics(R.drawable.ic_ledger_stax_error)
}
override fun getReviewAddressMessage(): String {
return resourceManager.getString(R.string.ledger_verify_address_message_confirm_button, getName())
}
override fun getReviewAddressesMessage(): String {
return resourceManager.getString(R.string.ledger_verify_addresses_message_confirm_button, getName())
}
override fun getSignMessage(): String {
return resourceManager.getString(R.string.ledger_hold_to_sign, getName())
}
}
@@ -0,0 +1,50 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.errors
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.mixin.api.Browserable
import io.novafoundation.nova.common.utils.event
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.SubstrateLedgerApplicationError
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommands
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
fun <V> V.handleLedgerError(
reason: Throwable,
device: LedgerDevice,
commandFormatter: MessageCommandFormatter,
onRetry: () -> Unit
) where V : BaseViewModel, V : LedgerMessageCommands, V : Browserable.Presentation {
reason.printStackTrace()
launch {
when (reason) {
is CancellationException -> {
// do nothing on coroutines cancellation
}
is SubstrateLedgerApplicationError.Response -> {
ledgerMessageCommands.value = commandFormatter.substrateApplicationError(
reason = reason.response,
device = device,
onCancel = ::hide,
onRetry = onRetry
).event()
}
else -> {
ledgerMessageCommands.value = commandFormatter.unknownError(
device = device,
onRetry = onRetry,
onCancel = ::hide
).event()
}
}
}
}
private fun LedgerMessageCommands.hide() {
ledgerMessageCommands.value = LedgerMessageCommand.Hide.event()
}
@@ -0,0 +1,21 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters
import io.novafoundation.nova.common.mixin.api.Browserable
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.view.AlertModel
import io.novafoundation.nova.feature_ledger_impl.R
class GenericLedgerMessageFormatter(
private val resourceManager: ResourceManager,
) : LedgerMessageFormatter {
override suspend fun appName(): String {
return resourceManager.getString(R.string.account_ledger_migration_generic)
}
context(Browserable.Presentation)
override suspend fun alertForKind(messageKind: LedgerMessageFormatter.MessageKind): AlertModel? {
// We do not show any alerts for new ledger app
return null
}
}
@@ -0,0 +1,18 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters
import io.novafoundation.nova.common.mixin.api.Browserable
import io.novafoundation.nova.common.view.AlertModel
interface LedgerMessageFormatter {
enum class MessageKind {
APP_NOT_OPEN, OTHER
}
suspend fun appName(): String
context(Browserable.Presentation)
suspend fun alertForKind(
messageKind: MessageKind,
): AlertModel?
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_ledger_core.domain.LedgerMigrationTracker
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
class LedgerMessageFormatterFactory(
private val resourceManager: ResourceManager,
private val migrationTracker: LedgerMigrationTracker,
private val chainRegistry: ChainRegistry,
private val appLinksProvider: AppLinksProvider,
) {
fun createLegacy(chainId: ChainId, showAlerts: Boolean): LedgerMessageFormatter {
return LegacyLedgerMessageFormatter(migrationTracker, resourceManager, chainRegistry, appLinksProvider, chainId, showAlerts)
}
fun createGeneric(): LedgerMessageFormatter {
return GenericLedgerMessageFormatter(resourceManager)
}
}
@@ -0,0 +1,73 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters
import io.novafoundation.nova.common.data.network.AppLinksProvider
import io.novafoundation.nova.common.mixin.api.Browserable
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.view.AlertModel
import io.novafoundation.nova.common.view.AlertModel.ActionModel
import io.novafoundation.nova.common.view.AlertView.StylePreset
import io.novafoundation.nova.common.view.asStyle
import io.novafoundation.nova.feature_ledger_core.domain.LedgerMigrationTracker
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class LegacyLedgerMessageFormatter(
private val migrationTracker: LedgerMigrationTracker,
private val resourceManager: ResourceManager,
private val chainRegistry: ChainRegistry,
private val appLinksProvider: AppLinksProvider,
private val chainId: ChainId,
private val showAlerts: Boolean
) : LedgerMessageFormatter {
private var shouldUseMigrationApp: Boolean? = null
private val cacheMutex = Mutex()
override suspend fun appName(): String {
return if (shouldUseMigrationApp()) {
resourceManager.getString(R.string.account_ledger_migration_app)
} else {
val chain = chainRegistry.getChain(chainId)
chain.name
}
}
context(Browserable.Presentation)
override suspend fun alertForKind(messageKind: LedgerMessageFormatter.MessageKind): AlertModel? {
val shouldShowAlert = showAlerts && shouldUseMigrationApp()
if (!shouldShowAlert) return null
return when (messageKind) {
LedgerMessageFormatter.MessageKind.APP_NOT_OPEN -> AlertModel(
style = StylePreset.INFO.asStyle(),
message = resourceManager.getString(R.string.account_ledger_legacy_warning_title),
subMessage = resourceManager.getString(R.string.account_ledger_legacy_warning_message),
linkAction = ActionModel(
text = resourceManager.getString(R.string.common_find_out_more),
listener = { showBrowser(appLinksProvider.ledgerMigrationArticle) }
)
)
LedgerMessageFormatter.MessageKind.OTHER -> AlertModel(
style = StylePreset.INFO.asStyle(),
message = resourceManager.getString(R.string.account_ledger_legacy_warning_title),
subMessage = resourceManager.getString(R.string.account_ledger_migration_deprecation_message),
linkAction = ActionModel(
text = resourceManager.getString(R.string.common_find_out_more),
listener = { showBrowser(appLinksProvider.ledgerMigrationArticle) }
)
)
}
}
private suspend fun shouldUseMigrationApp(): Boolean = cacheMutex.withLock {
if (shouldUseMigrationApp == null) {
shouldUseMigrationApp = migrationTracker.shouldUseMigrationApp(chainId)
}
shouldUseMigrationApp!!
}
}
@@ -0,0 +1,27 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.generic
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.view.AlertModel
import io.novafoundation.nova.common.view.AlertView
import io.novafoundation.nova.feature_ledger_impl.R
import javax.inject.Inject
interface GenericLedgerEvmAlertFormatter {
fun createUpdateAppToGetEvmAddressAlert(): AlertModel
}
@FeatureScope
class RealGenericLedgerEvmAlertFormatter @Inject constructor(
private val resourceManager: ResourceManager,
) : GenericLedgerEvmAlertFormatter {
override fun createUpdateAppToGetEvmAddressAlert(): AlertModel {
return AlertModel(
style = AlertView.Style.fromPreset(AlertView.StylePreset.WARNING),
message = resourceManager.getString(R.string.ledger_select_address_update_for_evm_title),
subMessage = resourceManager.getString(R.string.ledger_select_address_update_for_evm_message)
)
}
}
@@ -0,0 +1,81 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.ConcatAdapter
import coil.ImageLoader
import io.novafoundation.nova.common.address.AddressModel
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.view.setModelOrHide
import io.novafoundation.nova.feature_account_api.presenatation.addressActions.setupAddressActions
import io.novafoundation.nova.feature_ledger_impl.databinding.FragmentImportLedgerSelectAddressBinding
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessagePresentable
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.setupLedgerMessages
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.list.LedgerAccountAdapter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.list.LedgerSelectAddressLoadMoreAdapter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.model.LedgerAccountRvItem
import javax.inject.Inject
abstract class SelectAddressLedgerFragment<V : SelectAddressLedgerViewModel> :
BaseFragment<V, FragmentImportLedgerSelectAddressBinding>(),
LedgerSelectAddressLoadMoreAdapter.Handler,
LedgerAccountAdapter.Handler {
companion object {
private const val PAYLOAD_KEY = "SelectAddressImportLedgerFragment.PAYLOAD_KEY"
fun getBundle(payload: SelectLedgerAddressPayload) = bundleOf(PAYLOAD_KEY to payload)
}
@Inject
lateinit var imageLoader: ImageLoader
override fun createBinding() = FragmentImportLedgerSelectAddressBinding.inflate(layoutInflater)
private val addressesAdapter by lazy(LazyThreadSafetyMode.NONE) {
LedgerAccountAdapter(this)
}
private val loadMoreAdapter = LedgerSelectAddressLoadMoreAdapter(handler = this, lifecycleOwner = this)
@Inject
lateinit var ledgerMessagePresentable: LedgerMessagePresentable
protected fun payload(): SelectLedgerAddressPayload = argument(PAYLOAD_KEY)
override fun initViews() {
binder.ledgerSelectAddressToolbar.setHomeButtonListener {
viewModel.backClicked()
}
onBackPressed { viewModel.backClicked() }
binder.ledgerSelectAddressContent.setHasFixedSize(true)
binder.ledgerSelectAddressContent.adapter = ConcatAdapter(addressesAdapter, loadMoreAdapter)
}
override fun subscribe(viewModel: V) {
viewModel.loadMoreState.observe(loadMoreAdapter::setState)
viewModel.loadedAccountModels.observe(addressesAdapter::submitList)
viewModel.chainUi.observe(binder.ledgerSelectAddressChain::setChain)
viewModel.alertFlow.observe(binder.ledgerSelectAddressAlert::setModelOrHide)
setupLedgerMessages(ledgerMessagePresentable)
viewModel.addressActionsMixin.setupAddressActions()
}
override fun loadMoreClicked() {
viewModel.loadMoreClicked()
}
override fun itemClicked(item: LedgerAccountRvItem) {
viewModel.accountClicked(item)
}
override fun addressInfoClicked(addressModel: AddressModel, addressScheme: AddressScheme) {
viewModel.addressInfoClicked(addressModel, addressScheme)
}
}
@@ -0,0 +1,228 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress
import android.util.Log
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.address.AddressIconGenerator
import io.novafoundation.nova.common.address.AddressModel
import io.novafoundation.nova.common.address.format.AddressFormat
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.mixin.api.Browserable
import io.novafoundation.nova.common.presentation.DescriptiveButtonState
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.Event
import io.novafoundation.nova.common.utils.GENERIC_ADDRESS_PREFIX
import io.novafoundation.nova.common.utils.added
import io.novafoundation.nova.common.utils.event
import io.novafoundation.nova.common.utils.flowOf
import io.novafoundation.nova.common.utils.formatting.format
import io.novafoundation.nova.common.utils.invoke
import io.novafoundation.nova.common.utils.launchUnit
import io.novafoundation.nova.common.utils.lazyAsync
import io.novafoundation.nova.common.utils.mapList
import io.novafoundation.nova.common.view.AlertModel
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
import io.novafoundation.nova.feature_account_api.domain.model.LedgerVariant
import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAccountAddressModel
import io.novafoundation.nova.feature_account_api.presenatation.addressActions.AddressActionsMixin
import io.novafoundation.nova.feature_account_api.presenatation.addressActions.showAddressActions
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.address
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.LedgerAccount
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.SelectAddressLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommands
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.createLedgerReviewAddresses
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.errors.handleLedgerError
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.model.AddressVerificationMode
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.model.LedgerAccountRvItem
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novasama.substrate_sdk_android.ss58.SS58Encoder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class SelectAddressLedgerViewModel(
private val router: LedgerRouter,
protected val interactor: SelectAddressLedgerInteractor,
private val addressIconGenerator: AddressIconGenerator,
private val resourceManager: ResourceManager,
private val payload: SelectLedgerAddressPayload,
private val chainRegistry: ChainRegistry,
private val messageCommandFormatter: MessageCommandFormatter,
private val addressActionsMixinFactory: AddressActionsMixin.Factory
) : BaseViewModel(),
LedgerMessageCommands,
Browserable.Presentation by Browserable() {
abstract val ledgerVariant: LedgerVariant
abstract val addressVerificationMode: AddressVerificationMode
override val ledgerMessageCommands: MutableLiveData<Event<LedgerMessageCommand>> = MutableLiveData()
private val substratePreviewChain by lazyAsync { chainRegistry.getChain(payload.substrateChainId) }
private val loadingState = MutableStateFlow(AccountLoadingState.CAN_LOAD)
protected val loadedAccounts: MutableStateFlow<List<LedgerAccount>> = MutableStateFlow(emptyList())
private var verifyAddressJob: Job? = null
val loadedAccountModels = loadedAccounts.mapList { it.toUi() }
.shareInBackground()
val chainUi = flowOf { mapChainToUi(substratePreviewChain()) }
.shareInBackground()
val loadMoreState = loadingState.map { loadingState ->
when (loadingState) {
AccountLoadingState.CAN_LOAD -> DescriptiveButtonState.Enabled(resourceManager.getString(R.string.ledger_import_select_address_load_more))
AccountLoadingState.LOADING -> DescriptiveButtonState.Loading
AccountLoadingState.NOTHING_TO_LOAD -> DescriptiveButtonState.Gone
}
}.shareInBackground()
protected val _alertFlow = MutableStateFlow<AlertModel?>(null)
val alertFlow: Flow<AlertModel?> = _alertFlow
val device = flowOf {
interactor.getDevice(payload.deviceId)
}
val addressActionsMixin = addressActionsMixinFactory.create(this)
init {
loadNewAccount()
}
abstract fun onAccountVerified(account: LedgerAccount)
/**
* Loads ledger account. Can return Success(null) to indicate there is nothing to load and "load more" button should be hidden
*/
protected open suspend fun loadLedgerAccount(
substratePreviewChain: Chain,
deviceId: String,
accountIndex: Int,
ledgerVariant: LedgerVariant
): Result<LedgerAccount?> {
return interactor.loadLedgerAccount(substratePreviewChain, deviceId, accountIndex, ledgerVariant)
}
fun loadMoreClicked() {
if (loadingState.value != AccountLoadingState.CAN_LOAD) return
loadNewAccount()
}
fun backClicked() {
router.back()
}
fun accountClicked(accountUi: LedgerAccountRvItem) {
verifyAccount(accountUi.id)
}
fun addressInfoClicked(addressModel: AddressModel, addressScheme: AddressScheme) {
addressActionsMixin.showAddressActions(addressModel.address, AddressFormat.defaultForScheme(addressScheme, SS58Encoder.GENERIC_ADDRESS_PREFIX))
}
private fun verifyAccount(id: Int) {
verifyAddressJob?.cancel()
verifyAddressJob = launch {
val account = loadedAccounts.value.first { it.index == id }
val verificationMode = addressVerificationMode
if (verificationMode is AddressVerificationMode.Enabled) {
verifyAccountInternal(account, verificationMode.addressSchemesToVerify)
} else {
onAccountVerified(account)
}
}
}
private suspend fun verifyAccountInternal(account: LedgerAccount, reviewAddressSchemes: List<AddressScheme>) {
val device = device.first()
ledgerMessageCommands.value = messageCommandFormatter.reviewAddressCommand(
addresses = createLedgerReviewAddresses(
allowedAddressSchemes = reviewAddressSchemes,
AddressScheme.SUBSTRATE to account.substrate.address,
AddressScheme.EVM to account.evm?.address()
),
device = device,
onCancel = ::verifyAddressCancelled,
).event()
val result = withContext(Dispatchers.Default) {
interactor.verifyLedgerAccount(substratePreviewChain(), payload.deviceId, account.index, ledgerVariant, reviewAddressSchemes)
}
result.onFailure {
handleLedgerError(it, device) { verifyAccount(account.index) }
}.onSuccess {
ledgerMessageCommands.value = messageCommandFormatter.hideCommand().event()
onAccountVerified(account)
}
}
private fun handleLedgerError(error: Throwable, device: LedgerDevice, retry: () -> Unit) {
handleLedgerError(
reason = error,
device = device,
commandFormatter = messageCommandFormatter,
onRetry = retry
)
}
private fun verifyAddressCancelled() {
ledgerMessageCommands.value = messageCommandFormatter.hideCommand().event()
verifyAddressJob?.cancel()
verifyAddressJob = null
}
private fun loadNewAccount(): Unit = launchUnit(Dispatchers.Default) {
ledgerMessageCommands.postValue(messageCommandFormatter.hideCommand().event())
loadingState.value = AccountLoadingState.LOADING
val nextAccountIndex = loadedAccounts.value.size
loadLedgerAccount(substratePreviewChain(), payload.deviceId, nextAccountIndex, ledgerVariant)
.onSuccess { newAccount ->
if (newAccount != null) {
loadedAccounts.value = loadedAccounts.value.added(newAccount)
loadingState.value = AccountLoadingState.CAN_LOAD
} else {
loadingState.value = AccountLoadingState.NOTHING_TO_LOAD
}
}.onFailure {
Log.e("Ledger", "Failed to load Ledger account", it)
handleLedgerError(it, device.first()) { loadNewAccount() }
loadingState.value = AccountLoadingState.CAN_LOAD
}
}
private suspend fun LedgerAccount.toUi(): LedgerAccountRvItem {
return LedgerAccountRvItem(
id = index,
label = resourceManager.getString(R.string.ledger_select_address_account_label, (index + 1).format()),
substrate = addressIconGenerator.createAccountAddressModel(substratePreviewChain(), substrate.address),
evm = evm?.let { addressIconGenerator.createAccountAddressModel(AddressFormat.evm(), it.accountId) }
)
}
enum class AccountLoadingState {
CAN_LOAD, LOADING, NOTHING_TO_LOAD
}
}
@@ -0,0 +1,11 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress
import android.os.Parcelable
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import kotlinx.parcelize.Parcelize
@Parcelize
class SelectLedgerAddressPayload(
val deviceId: String,
val substrateChainId: ChainId
) : Parcelable
@@ -0,0 +1,89 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.list
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import io.novafoundation.nova.common.address.AddressModel
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.common.list.BaseListAdapter
import io.novafoundation.nova.common.list.BaseViewHolder
import io.novafoundation.nova.common.utils.inflater
import io.novafoundation.nova.common.utils.setTextColorRes
import io.novafoundation.nova.common.view.setExtraInfoAvailable
import io.novafoundation.nova.common.view.shape.addRipple
import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable
import io.novafoundation.nova.feature_account_api.view.showAddress
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.databinding.ItemLedgerAccountBinding
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.model.LedgerAccountRvItem
class LedgerAccountAdapter(
private val handler: Handler
) : BaseListAdapter<LedgerAccountRvItem, SelectLedgerHolder>(DiffCallback()) {
interface Handler {
fun itemClicked(item: LedgerAccountRvItem)
fun addressInfoClicked(addressModel: AddressModel, addressScheme: AddressScheme)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectLedgerHolder {
return SelectLedgerHolder(ItemLedgerAccountBinding.inflate(parent.inflater(), parent, false), handler)
}
override fun onBindViewHolder(holder: SelectLedgerHolder, position: Int) {
holder.bind(getItem(position))
}
}
private class DiffCallback : DiffUtil.ItemCallback<LedgerAccountRvItem>() {
override fun areItemsTheSame(oldItem: LedgerAccountRvItem, newItem: LedgerAccountRvItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: LedgerAccountRvItem, newItem: LedgerAccountRvItem): Boolean {
return oldItem == newItem
}
}
class SelectLedgerHolder(
private val viewBinding: ItemLedgerAccountBinding,
private val eventHandler: LedgerAccountAdapter.Handler
) : BaseViewHolder(viewBinding.root) {
init {
viewBinding.root.background = with(containerView.context) {
addRipple(getRoundedCornerDrawable(R.color.block_background))
}
viewBinding.itemLedgerAccountSubstrate.setExtraInfoAvailable(true)
}
fun bind(model: LedgerAccountRvItem) = with(viewBinding) {
viewBinding.root.setOnClickListener { eventHandler.itemClicked(model) }
itemLedgerAccountLabel.text = model.label
itemLedgerAccountIcon.setImageDrawable(model.substrate.image)
itemLedgerAccountSubstrate.showAddress(model.substrate)
itemLedgerAccountSubstrate.setOnClickListener { eventHandler.addressInfoClicked(model.substrate, AddressScheme.SUBSTRATE) }
if (model.evm != null) {
itemLedgerAccountEvm.valuePrimary.setTextColorRes(R.color.text_primary)
itemLedgerAccountEvm.setPrimaryValueStartIcon(null)
itemLedgerAccountEvm.showAddress(model.evm)
itemLedgerAccountEvm.setOnClickListener { eventHandler.addressInfoClicked(model.evm, AddressScheme.EVM) }
itemLedgerAccountEvm.setExtraInfoAvailable(true)
} else {
itemLedgerAccountEvm.valuePrimary.setTextColorRes(R.color.text_secondary)
itemLedgerAccountEvm.showValue(context.getString(R.string.ledger_select_address_not_found))
itemLedgerAccountEvm.setOnClickListener(null)
itemLedgerAccountEvm.setExtraInfoAvailable(false)
itemLedgerAccountEvm.setPrimaryValueStartIcon(R.drawable.ic_warning_filled)
}
}
override fun unbind() {}
}
@@ -0,0 +1,60 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.list
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import io.novafoundation.nova.common.list.BaseViewHolder
import io.novafoundation.nova.common.presentation.DescriptiveButtonState
import io.novafoundation.nova.common.utils.inflateChild
import io.novafoundation.nova.common.view.PrimaryButton
import io.novafoundation.nova.common.view.setState
import io.novafoundation.nova.feature_ledger_impl.R
class LedgerSelectAddressLoadMoreAdapter(
private val handler: Handler,
private val lifecycleOwner: LifecycleOwner,
) : RecyclerView.Adapter<LedgerSelectAddressLoadMoreViewHolder>() {
interface Handler {
fun loadMoreClicked()
}
private var state: DescriptiveButtonState? = null
fun setState(newState: DescriptiveButtonState) {
state = newState
notifyItemChanged(0)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LedgerSelectAddressLoadMoreViewHolder {
val containerView = parent.inflateChild(R.layout.item_select_address_load_more) as PrimaryButton
return LedgerSelectAddressLoadMoreViewHolder(containerView, handler, lifecycleOwner)
}
override fun onBindViewHolder(holder: LedgerSelectAddressLoadMoreViewHolder, position: Int) {
state?.let(holder::bind)
}
override fun getItemCount(): Int = 1
}
class LedgerSelectAddressLoadMoreViewHolder(
override val containerView: PrimaryButton,
handler: LedgerSelectAddressLoadMoreAdapter.Handler,
lifecycleOwner: LifecycleOwner,
) : BaseViewHolder(containerView) {
init {
containerView.prepareForProgress(lifecycleOwner)
containerView.setOnClickListener { handler.loadMoreClicked() }
}
fun bind(state: DescriptiveButtonState) {
containerView.setState(state)
}
override fun unbind() {}
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.model
import io.novafoundation.nova.common.address.format.AddressScheme
sealed class AddressVerificationMode {
data object Disabled : AddressVerificationMode()
class Enabled(val addressSchemesToVerify: List<AddressScheme>) : AddressVerificationMode()
}
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.model
import io.novafoundation.nova.common.address.AddressModel
data class LedgerAccountRvItem(
val id: Int,
val label: String,
val substrate: AddressModel,
val evm: AddressModel?
)
@@ -0,0 +1,71 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import io.novafoundation.nova.common.list.BaseListAdapter
import io.novafoundation.nova.common.list.BaseViewHolder
import io.novafoundation.nova.common.utils.inflater
import io.novafoundation.nova.common.view.shape.addRipple
import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawable
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.databinding.ItemLedgerBinding
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.model.SelectLedgerModel
class SelectLedgerAdapter(
private val handler: Handler
) : BaseListAdapter<SelectLedgerModel, SelectLedgerHolder>(DiffCallback()) {
interface Handler {
fun itemClicked(item: SelectLedgerModel)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectLedgerHolder {
return SelectLedgerHolder(ItemLedgerBinding.inflate(parent.inflater(), parent, false), handler)
}
override fun onBindViewHolder(holder: SelectLedgerHolder, position: Int) {
holder.bind(getItem(position))
}
}
private class DiffCallback : DiffUtil.ItemCallback<SelectLedgerModel>() {
override fun areItemsTheSame(oldItem: SelectLedgerModel, newItem: SelectLedgerModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: SelectLedgerModel, newItem: SelectLedgerModel): Boolean {
return oldItem == newItem
}
}
class SelectLedgerHolder(
private val binder: ItemLedgerBinding,
private val eventHandler: SelectLedgerAdapter.Handler
) : BaseViewHolder(binder.root) {
init {
binder.itemLedger.background = with(containerView.context) {
addRipple(getRoundedCornerDrawable(R.color.block_background))
}
binder.itemLedger.setProgressTint(R.color.icon_secondary)
}
fun bind(model: SelectLedgerModel) = with(binder) {
itemLedger.title.text = model.name
bindConnecting(model)
}
fun bindConnecting(model: SelectLedgerModel) = with(binder) {
itemLedger.setInProgress(model.isConnecting)
if (model.isConnecting) {
root.setOnClickListener(null)
} else {
root.setOnClickListener { eventHandler.itemClicked(model) }
}
}
override fun unbind() {}
}
@@ -0,0 +1,133 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger
import android.bluetooth.BluetoothAdapter
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.location.LocationManager
import android.os.Bundle
import androidx.core.view.isVisible
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents
import io.novafoundation.nova.common.utils.permissions.setupPermissionAsker
import io.novafoundation.nova.common.utils.setVisible
import io.novafoundation.nova.common.view.dialog.dialog
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.databinding.FragmentSelectLedgerBinding
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessagePresentable
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.setupLedgerMessages
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.model.SelectLedgerModel
import javax.inject.Inject
abstract class SelectLedgerFragment<V : SelectLedgerViewModel> : BaseFragment<V, FragmentSelectLedgerBinding>(), SelectLedgerAdapter.Handler {
override fun createBinding() = FragmentSelectLedgerBinding.inflate(layoutInflater)
@Inject
lateinit var ledgerMessagePresentable: LedgerMessagePresentable
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
SelectLedgerAdapter(this)
}
private val bluetoothConnectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
BluetoothAdapter.STATE_OFF -> viewModel.bluetoothStateChanged(BluetoothState.OFF)
BluetoothAdapter.STATE_ON -> viewModel.bluetoothStateChanged(BluetoothState.ON)
}
}
}
private val locationStateReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
val action = intent.action
if (action != null && action == LocationManager.PROVIDERS_CHANGED_ACTION) {
viewModel.locationStateChanged()
}
}
}
override fun initViews() {
binder.selectLedgerToolbar.setHomeButtonListener { viewModel.backClicked() }
onBackPressed { viewModel.backClicked() }
binder.selectLedgerGrantPermissions.setOnClickListener { viewModel.allowAvailabilityRequests() }
binder.selectLedgerDevices.setHasFixedSize(true)
binder.selectLedgerDevices.adapter = adapter
}
override fun subscribe(viewModel: V) {
viewModel.deviceModels.observe {
adapter.submitList(it)
binder.selectLedgerDevices.setVisible(it.isNotEmpty())
binder.selectLedgerProgress.setVisible(it.isEmpty())
}
viewModel.showRequestLocationDialog.observe {
dialog(requireContext(), R.style.AccentAlertDialogTheme) {
setTitle(R.string.select_ledger_location_enable_request_title)
setMessage(getString(R.string.select_ledger_location_enable_request_message))
setPositiveButton(R.string.common_enable) { _, _ -> viewModel.enableLocationAcknowledged() }
setNegativeButton(R.string.common_cancel, null)
}
}
viewModel.hints.observe(binder.selectLedgerHints::setText)
viewModel.showPermissionsButton.observe { binder.selectLedgerGrantPermissions.isVisible = it }
setupPermissionAsker(viewModel)
setupLedgerMessages(ledgerMessagePresentable)
observeBrowserEvents(viewModel)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableBluetoothConnectivityTracker()
enableLocationStateTracker()
}
override fun onDestroy() {
super.onDestroy()
disableBluetoothConnectivityTracker()
disableLocationStateTracker()
}
private fun enableLocationStateTracker() {
val filter = IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION)
requireActivity().registerReceiver(locationStateReceiver, filter)
}
private fun disableLocationStateTracker() {
try {
requireActivity().unregisterReceiver(locationStateReceiver)
} catch (e: IllegalArgumentException) {
// Receiver not registered
}
}
private fun enableBluetoothConnectivityTracker() {
val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
requireActivity().registerReceiver(bluetoothConnectivityReceiver, filter)
}
private fun disableBluetoothConnectivityTracker() {
try {
requireActivity().unregisterReceiver(bluetoothConnectivityReceiver)
} catch (e: IllegalArgumentException) {
// Receiver not registered
}
}
override fun itemClicked(item: SelectLedgerModel) {
viewModel.deviceClicked(item)
}
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger
import android.os.Parcelable
interface SelectLedgerPayload : Parcelable {
val connectionMode: ConnectionMode
enum class ConnectionMode {
BLUETOOTH, USB, ALL
}
}
@@ -0,0 +1,315 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger
import android.Manifest
import android.os.Build
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.mixin.api.Browserable
import io.novafoundation.nova.common.navigation.ReturnableRouter
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.Event
import io.novafoundation.nova.common.utils.bluetooth.BluetoothManager
import io.novafoundation.nova.common.utils.flowOf
import io.novafoundation.nova.common.utils.location.LocationManager
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.common.utils.permissions.checkPermissions
import io.novafoundation.nova.common.utils.stateMachine.StateMachine
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryMethods
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryRequirement
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryRequirementAvailability
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.discoveryRequirements
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.findDevice
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommands
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers.LedgerDeviceFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.errors.handleLedgerError
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.model.SelectLedgerModel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SelectLedgerEvent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SideEffect
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.states.DevicesFoundState
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.states.DiscoveringState
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.states.SelectLedgerState
import io.novafoundation.nova.feature_ledger_impl.sdk.discovery.ble.BleScanFailed
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
enum class BluetoothState {
ON, OFF
}
abstract class SelectLedgerViewModel(
private val discoveryService: LedgerDeviceDiscoveryService,
private val permissionsAsker: PermissionsAsker.Presentation,
private val bluetoothManager: BluetoothManager,
private val locationManager: LocationManager,
private val router: ReturnableRouter,
private val resourceManager: ResourceManager,
private val messageFormatter: LedgerMessageFormatter,
private val payload: SelectLedgerPayload,
private val ledgerDeviceFormatter: LedgerDeviceFormatter,
private val messageCommandFormatter: MessageCommandFormatter,
) : BaseViewModel(),
PermissionsAsker by permissionsAsker,
LedgerMessageCommands,
Browserable.Presentation by Browserable() {
private val discoveryMethods = payload.connectionMode.toDiscoveryMethod()
private val stateMachine = StateMachine(createInitialState(), coroutineScope = this)
val deviceModels = stateMachine.state.map(::mapStateToUi)
.shareInBackground()
val hints = flowOf {
when (payload.connectionMode) {
SelectLedgerPayload.ConnectionMode.BLUETOOTH -> resourceManager.getString(
R.string.account_ledger_select_device_description,
messageFormatter.appName()
)
SelectLedgerPayload.ConnectionMode.USB -> resourceManager.getString(
R.string.account_ledger_select_device_usb_description,
messageFormatter.appName()
)
SelectLedgerPayload.ConnectionMode.ALL -> resourceManager.getString(R.string.account_ledger_select_device_all_description)
}
}.shareInBackground()
val showPermissionsButton = flowOf { payload.connectionMode == SelectLedgerPayload.ConnectionMode.ALL }
.shareInBackground()
override val ledgerMessageCommands = MutableLiveData<Event<LedgerMessageCommand>>()
private val _showRequestLocationDialog = MutableLiveData<Boolean>()
val showRequestLocationDialog: LiveData<Boolean> = _showRequestLocationDialog
init {
handleSideEffects()
setupDiscoveryObserving()
}
abstract suspend fun verifyConnection(device: LedgerDevice)
open suspend fun handleLedgerError(reason: Throwable, device: LedgerDevice) {
handleLedgerError(
reason = reason,
device = device,
commandFormatter = messageCommandFormatter,
onRetry = { stateMachine.onEvent(SelectLedgerEvent.DeviceChosen(device)) }
)
}
open fun backClicked() {
router.back()
}
fun allowAvailabilityRequests() {
stateMachine.onEvent(SelectLedgerEvent.AvailabilityRequestsAllowed)
}
fun deviceClicked(item: SelectLedgerModel) = launch {
discoveryService.findDevice(item.id)?.let { device ->
stateMachine.onEvent(SelectLedgerEvent.DeviceChosen(device))
}
}
fun bluetoothStateChanged(state: BluetoothState) {
when (state) {
BluetoothState.ON -> stateMachine.onEvent(SelectLedgerEvent.DiscoveryRequirementSatisfied(DiscoveryRequirement.BLUETOOTH))
BluetoothState.OFF -> stateMachine.onEvent(SelectLedgerEvent.DiscoveryRequirementMissing(DiscoveryRequirement.BLUETOOTH))
}
}
fun locationStateChanged() {
emitLocationState()
}
fun enableLocationAcknowledged() {
locationManager.enableLocation()
}
override fun onCleared() {
discoveryService.stopDiscovery()
}
private fun emitLocationState() {
when (locationManager.isLocationEnabled()) {
true -> stateMachine.onEvent(SelectLedgerEvent.DiscoveryRequirementSatisfied(DiscoveryRequirement.LOCATION))
false -> stateMachine.onEvent(SelectLedgerEvent.DiscoveryRequirementMissing(DiscoveryRequirement.LOCATION))
}
}
private fun handleSideEffects() {
launch {
for (effect in stateMachine.sideEffects) {
handleSideEffect(effect)
}
}
}
private fun performConnectionVerification(device: LedgerDevice) = launch {
runCatching { verifyConnection(device) }
.onSuccess { stateMachine.onEvent(SelectLedgerEvent.ConnectionVerified) }
.onFailure { stateMachine.onEvent(SelectLedgerEvent.VerificationFailed(it)) }
}
private suspend fun handleSideEffect(effect: SideEffect) {
when (effect) {
is SideEffect.PresentLedgerFailure -> launch { handleLedgerError(effect.reason, effect.device) }
is SideEffect.VerifyConnection -> performConnectionVerification(effect.device)
is SideEffect.StartDiscovery -> discoveryService.startDiscovery(effect.methods)
is SideEffect.RequestPermissions -> requestPermissions(effect.requirements, effect.shouldExitUponDenial)
is SideEffect.RequestSatisfyRequirement -> requestSatisfyRequirement(effect.requirements)
is SideEffect.StopDiscovery -> discoveryService.stopDiscovery(effect.methods)
}
}
private suspend fun requestSatisfyRequirement(requirements: List<DiscoveryRequirement>) {
val (awaitable, fireAndForget) = requirements.map { it.createRequest() }
.partition { it.awaitable }
// Do awaitable requests first to reduce the change of overlapping requests happening
// With this logic overlapping requests may only happen if there are more than one `fireAndForget` requirement
// Important: permissions are handled separately and state machine ensures permissions are always requested first
awaitable.forEach { it.requestAction() }
fireAndForget.forEach { it.requestAction() }
}
private suspend fun requestPermissions(
discoveryRequirements: List<DiscoveryRequirement>,
shouldExitUponDenial: Boolean
): Boolean {
val permissions = discoveryRequirements.requiredPermissions()
val granted = permissionsAsker.requirePermissions(*permissions.toTypedArray())
if (granted) {
stateMachine.onEvent(SelectLedgerEvent.PermissionsGranted)
} else {
onPermissionsNotGranted(shouldExitUponDenial)
}
return granted
}
private fun setupDiscoveryObserving() {
discoveryService.errors.onEach(::discoveryError)
discoveryService.discoveredDevices.onEach {
stateMachine.onEvent(SelectLedgerEvent.DiscoveredDevicesListChanged(it))
}.launchIn(this)
}
private fun onPermissionsNotGranted(shouldExitUponDenial: Boolean) {
if (shouldExitUponDenial) {
router.back()
}
}
private fun discoveryError(error: Throwable) {
when (error) {
is BleScanFailed -> {
val event = SelectLedgerEvent.DiscoveryRequirementMissing(DiscoveryRequirement.BLUETOOTH)
stateMachine.onEvent(event)
}
}
}
private fun mapStateToUi(state: SelectLedgerState): List<SelectLedgerModel> {
return when (state) {
is DevicesFoundState -> mapDevicesToUi(state.devices, connectingTo = state.verifyingDevice)
is DiscoveringState -> emptyList()
}
}
private fun mapDevicesToUi(devices: List<LedgerDevice>, connectingTo: LedgerDevice?): List<SelectLedgerModel> {
return devices.map {
SelectLedgerModel(
id = it.id,
name = ledgerDeviceFormatter.formatName(it),
isConnecting = it.id == connectingTo?.id
)
}
}
private fun SelectLedgerPayload.ConnectionMode.toDiscoveryMethod(): DiscoveryMethods {
return when (this) {
SelectLedgerPayload.ConnectionMode.BLUETOOTH -> DiscoveryMethods(DiscoveryMethods.Method.BLE)
SelectLedgerPayload.ConnectionMode.USB -> DiscoveryMethods(DiscoveryMethods.Method.USB)
SelectLedgerPayload.ConnectionMode.ALL -> DiscoveryMethods(DiscoveryMethods.Method.BLE, DiscoveryMethods.Method.USB)
}
}
private fun List<DiscoveryRequirement>.requiredPermissions() = flatMap { it.requiredPermissions() }
private fun DiscoveryRequirement.requiredPermissions() = when (this) {
DiscoveryRequirement.BLUETOOTH -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
listOf(
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
)
} else {
emptyList()
}
}
DiscoveryRequirement.LOCATION -> listOf(Manifest.permission.ACCESS_FINE_LOCATION)
}
private fun createInitialState(): SelectLedgerState {
val allRequirements = discoveryMethods.discoveryRequirements()
val permissionsGranted = permissionsAsker.checkPermissions(allRequirements.requiredPermissions())
val satisfiedDiscoverRequirements = setOfNotNull(
DiscoveryRequirement.BLUETOOTH.takeIf { bluetoothManager.isBluetoothEnabled() },
DiscoveryRequirement.LOCATION.takeIf { locationManager.isLocationEnabled() }
)
val availability = DiscoveryRequirementAvailability(satisfiedDiscoverRequirements, permissionsGranted)
return DiscoveringState.initial(discoveryMethods, availability)
}
private fun DiscoveryRequirement.createRequest(): DiscoveryRequirementRequest {
return when (this) {
DiscoveryRequirement.BLUETOOTH -> DiscoveryRequirementRequest.awaitable { bluetoothManager.enableBluetoothAndAwait() }
DiscoveryRequirement.LOCATION -> DiscoveryRequirementRequest.fireAndForget { requestLocation() }
}
}
private fun requestLocation() {
_showRequestLocationDialog.value = true
}
private class DiscoveryRequirementRequest(
val requestAction: suspend () -> Unit,
val awaitable: Boolean
) {
companion object {
fun awaitable(requestAction: suspend () -> Unit): DiscoveryRequirementRequest {
return DiscoveryRequirementRequest(requestAction, awaitable = true)
}
fun fireAndForget(requestAction: () -> Unit): DiscoveryRequirementRequest {
return DiscoveryRequirementRequest(requestAction, awaitable = false)
}
}
}
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.model
data class SelectLedgerModel(
val name: String,
val id: String,
val isConnecting: Boolean,
)
@@ -0,0 +1,51 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryMethods
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryRequirement
sealed class SideEffect {
data class RequestPermissions(val requirements: List<DiscoveryRequirement>, val shouldExitUponDenial: Boolean) : SideEffect()
data class RequestSatisfyRequirement(val requirements: List<DiscoveryRequirement>) : SideEffect()
data class PresentLedgerFailure(val reason: Throwable, val device: LedgerDevice) : SideEffect()
data class VerifyConnection(val device: LedgerDevice) : SideEffect()
data class StartDiscovery(val methods: Set<DiscoveryMethods.Method>) : SideEffect()
data class StopDiscovery(val methods: Set<DiscoveryMethods.Method>) : SideEffect()
}
sealed class SelectLedgerEvent {
data class DiscoveryRequirementSatisfied(val requirement: DiscoveryRequirement) : SelectLedgerEvent()
data class DiscoveryRequirementMissing(val requirement: DiscoveryRequirement) : SelectLedgerEvent()
object PermissionsGranted : SelectLedgerEvent() {
override fun toString(): String {
return "PermissionsGranted"
}
}
object AvailabilityRequestsAllowed : SelectLedgerEvent() {
override fun toString(): String {
return "AvailabilityRequestsAllowed"
}
}
data class DiscoveredDevicesListChanged(val newDevices: List<LedgerDevice>) : SelectLedgerEvent()
data class DeviceChosen(val device: LedgerDevice) : SelectLedgerEvent()
data class VerificationFailed(val reason: Throwable) : SelectLedgerEvent()
object ConnectionVerified : SelectLedgerEvent() {
override fun toString(): String {
return "ConnectionVerified"
}
}
}
@@ -0,0 +1,65 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.states
import io.novafoundation.nova.common.utils.stateMachine.StateMachine
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryMethods
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryRequirementAvailability
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SelectLedgerEvent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SelectLedgerEvent.ConnectionVerified
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SelectLedgerEvent.DeviceChosen
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SelectLedgerEvent.DiscoveredDevicesListChanged
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SelectLedgerEvent.VerificationFailed
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SideEffect
data class DevicesFoundState(
val devices: List<LedgerDevice>,
val verifyingDevice: LedgerDevice?,
private val discoveryMethods: DiscoveryMethods,
private val discoveryRequirementAvailability: DiscoveryRequirementAvailability,
private val usedAllowedRequirementAvailabilityRequests: Boolean
) : SelectLedgerState() {
context(StateMachine.Transition<SelectLedgerState, SideEffect>)
override suspend fun performTransition(event: SelectLedgerEvent) {
when (event) {
is SelectLedgerEvent.AvailabilityRequestsAllowed -> userAllowedAvailabilityRequests(
discoveryMethods = discoveryMethods,
discoveryRequirementAvailability = discoveryRequirementAvailability,
nextState = copy(usedAllowedRequirementAvailabilityRequests = true)
)
is VerificationFailed -> verifyingDevice?.let { device ->
emitState(copy(verifyingDevice = null))
emitSideEffect(SideEffect.PresentLedgerFailure(event.reason, device))
}
is ConnectionVerified -> verifyingDevice?.let {
emitState(
DevicesFoundState(
devices = devices,
verifyingDevice = null,
discoveryMethods = discoveryMethods,
discoveryRequirementAvailability = discoveryRequirementAvailability,
usedAllowedRequirementAvailabilityRequests = usedAllowedRequirementAvailabilityRequests
)
)
}
is DeviceChosen -> {
emitState(copy(verifyingDevice = event.device))
emitSideEffect(SideEffect.VerifyConnection(event.device))
}
is DiscoveredDevicesListChanged -> emitState(copy(devices = event.newDevices))
else -> updateActiveDiscoveryMethodsByEvent(
allDiscoveryMethods = discoveryMethods,
previousRequirementsAvailability = discoveryRequirementAvailability,
event = event,
userAllowedRequirementAvailabilityRequests = usedAllowedRequirementAvailabilityRequests
) { newSatisfiedRequirements ->
DevicesFoundState(devices, verifyingDevice, discoveryMethods, newSatisfiedRequirements, usedAllowedRequirementAvailabilityRequests)
}
}
}
}
@@ -0,0 +1,69 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.states
import io.novafoundation.nova.common.utils.stateMachine.StateMachine
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryMethods
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryRequirementAvailability
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SelectLedgerEvent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SelectLedgerEvent.DiscoveredDevicesListChanged
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SideEffect
data class DiscoveringState(
private val discoveryMethods: DiscoveryMethods,
private val discoveryRequirementAvailability: DiscoveryRequirementAvailability,
private val usedAllowedRequirementAvailabilityRequests: Boolean
) : SelectLedgerState() {
companion object {
fun initial(discoveryMethods: DiscoveryMethods, initialRequirementAvailability: DiscoveryRequirementAvailability): DiscoveringState {
return DiscoveringState(discoveryMethods, initialRequirementAvailability, usedAllowedRequirementAvailabilityRequests = false)
}
}
context(StateMachine.Transition<SelectLedgerState, SideEffect>)
override suspend fun performTransition(event: SelectLedgerEvent) {
when (event) {
is SelectLedgerEvent.AvailabilityRequestsAllowed -> userAllowedAvailabilityRequests(
discoveryMethods = discoveryMethods,
discoveryRequirementAvailability = discoveryRequirementAvailability,
nextState = copy(usedAllowedRequirementAvailabilityRequests = true)
)
is DiscoveredDevicesListChanged -> if (event.newDevices.isNotEmpty()) {
val newState = DevicesFoundState(
devices = event.newDevices,
verifyingDevice = null,
discoveryMethods = discoveryMethods,
discoveryRequirementAvailability = discoveryRequirementAvailability,
usedAllowedRequirementAvailabilityRequests = usedAllowedRequirementAvailabilityRequests
)
emitState(newState)
}
else -> updateActiveDiscoveryMethodsByEvent(
allDiscoveryMethods = discoveryMethods,
previousRequirementsAvailability = discoveryRequirementAvailability,
event = event,
userAllowedRequirementAvailabilityRequests = usedAllowedRequirementAvailabilityRequests
) { newDiscoveryRequirementAvailability ->
DiscoveringState(discoveryMethods, newDiscoveryRequirementAvailability, usedAllowedRequirementAvailabilityRequests)
}
}
}
context(StateMachine.Transition<SelectLedgerState, SideEffect>)
override suspend fun bootstrap() {
// Start discovery for all methods available from the start
// I.e. those that do not have any requirements or already have all permissions / requirements satisfied
updateActiveDiscoveryMethods(
allDiscoveryMethods = discoveryMethods,
previousActiveMethods = emptySet(),
newRequirementsAvailability = discoveryRequirementAvailability
)
// Perform initial automatic requests to requirements and permissions if needed
// It's important to request permissions firstly since bluetooth request will crash application without permissions after android 12
requestPermissions(discoveryMethods, discoveryRequirementAvailability, usedAllowedRequirementAvailabilityRequests)
requestMissingDiscoveryRequirements(discoveryMethods, discoveryRequirementAvailability, usedAllowedRequirementAvailabilityRequests)
}
}
@@ -0,0 +1,168 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.states
import io.novafoundation.nova.common.utils.stateMachine.StateMachine
import io.novafoundation.nova.common.utils.stateMachine.StateMachine.Transition
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryMethods
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryRequirement
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryRequirementAvailability
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.discoveryRequirements
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.filterBySatisfiedRequirements
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.grantPermissions
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.missRequirement
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.satisfyRequirement
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SelectLedgerEvent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.stateMachine.SideEffect
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.DiscoveryMethods.Method as DiscoveryMethod
sealed class SelectLedgerState : StateMachine.State<SelectLedgerState, SideEffect, SelectLedgerEvent> {
context(Transition<SelectLedgerState, SideEffect>)
protected suspend fun updateActiveDiscoveryMethodsByEvent(
allDiscoveryMethods: DiscoveryMethods,
previousRequirementsAvailability: DiscoveryRequirementAvailability,
event: SelectLedgerEvent,
userAllowedRequirementAvailabilityRequests: Boolean,
newState: (newRequirementsAvailability: DiscoveryRequirementAvailability) -> SelectLedgerState
) {
val newPreviousSatisfiedRequirements = updateDiscoveryRequirementAvailabilityByEvent(previousRequirementsAvailability, event) ?: return
emitState(newState(newPreviousSatisfiedRequirements))
updateActiveDiscoveryMethods(
allDiscoveryMethods,
previousRequirementsAvailability,
newPreviousSatisfiedRequirements,
)
if (event is SelectLedgerEvent.DiscoveryRequirementMissing) {
requestSatisfyDiscoveryRequirement(allDiscoveryMethods, event.requirement, userAllowedRequirementAvailabilityRequests)
}
}
context(Transition<SelectLedgerState, SideEffect>)
protected suspend fun userAllowedAvailabilityRequests(
discoveryMethods: DiscoveryMethods,
discoveryRequirementAvailability: DiscoveryRequirementAvailability,
nextState: SelectLedgerState,
) {
emitState(nextState)
requestPermissions(discoveryMethods, discoveryRequirementAvailability, usedAllowedRequirementAvailabilityRequests = true)
requestMissingDiscoveryRequirements(discoveryMethods, discoveryRequirementAvailability, usedAllowedRequirementAvailabilityRequests = true)
}
context(Transition<SelectLedgerState, SideEffect>)
protected suspend fun updateActiveDiscoveryMethods(
allDiscoveryMethods: DiscoveryMethods,
previousRequirementsAvailability: DiscoveryRequirementAvailability,
newRequirementsAvailability: DiscoveryRequirementAvailability,
) {
val previousActiveDiscoveryMethods = allDiscoveryMethods.filterBySatisfiedRequirements(previousRequirementsAvailability)
updateActiveDiscoveryMethods(
allDiscoveryMethods,
previousActiveDiscoveryMethods,
newRequirementsAvailability,
)
}
context(Transition<SelectLedgerState, SideEffect>)
protected suspend fun updateActiveDiscoveryMethods(
allDiscoveryMethods: DiscoveryMethods,
previousActiveMethods: Set<DiscoveryMethod>,
newRequirementsAvailability: DiscoveryRequirementAvailability,
) {
val newActiveMethods = allDiscoveryMethods.filterBySatisfiedRequirements(newRequirementsAvailability)
val methodsToStart = newActiveMethods - previousActiveMethods
val methodsToStop = previousActiveMethods - newActiveMethods
startDiscovery(methodsToStart)
stopDiscovery(methodsToStop)
}
context(Transition<SelectLedgerState, SideEffect>)
protected suspend fun requestPermissions(
allDiscoveryMethods: DiscoveryMethods,
newRequirementsAvailability: DiscoveryRequirementAvailability,
usedAllowedRequirementAvailabilityRequests: Boolean
) {
val allDiscoveryRequirements = allDiscoveryMethods.discoveryRequirements()
val canRequestPermissions = canPerformAvailabilityRequests(allDiscoveryMethods, usedAllowedRequirementAvailabilityRequests)
// We only need permissions when there is at least one requirement (assuming each requirement requires at least one permission)
// and permissions has not been granted yet
val permissionsNeeded = allDiscoveryRequirements.isNotEmpty() && !newRequirementsAvailability.permissionsGranted
if (canRequestPermissions && permissionsNeeded) {
val shouldExitUponDenial = allDiscoveryMethods.methods.size == 1
emitSideEffect(SideEffect.RequestPermissions(allDiscoveryRequirements, shouldExitUponDenial))
}
}
context(Transition<SelectLedgerState, SideEffect>)
protected suspend fun requestMissingDiscoveryRequirements(
discoveryMethods: DiscoveryMethods,
discoveryRequirementAvailability: DiscoveryRequirementAvailability,
usedAllowedRequirementAvailabilityRequests: Boolean
) {
val canRequestRequirement = canPerformAvailabilityRequests(discoveryMethods, usedAllowedRequirementAvailabilityRequests)
if (!canRequestRequirement) return
val allRequirements = discoveryMethods.discoveryRequirements()
val missingRequirements = allRequirements - discoveryRequirementAvailability.satisfiedRequirements
if (missingRequirements.isNotEmpty()) {
emitSideEffect(SideEffect.RequestSatisfyRequirement(missingRequirements))
}
}
context(Transition<SelectLedgerState, SideEffect>)
private suspend fun requestSatisfyDiscoveryRequirement(
allDiscoveryMethods: DiscoveryMethods,
requirement: DiscoveryRequirement,
usedAllowedRequirementAvailabilityRequests: Boolean
) {
val canRequestRequirement = canPerformAvailabilityRequests(allDiscoveryMethods, usedAllowedRequirementAvailabilityRequests)
if (canRequestRequirement) {
emitSideEffect(SideEffect.RequestSatisfyRequirement(listOf(requirement)))
}
}
private fun canPerformAvailabilityRequests(
allDiscoveryMethods: DiscoveryMethods,
usedAllowedRequirementAvailabilityRequests: Boolean
): Boolean {
// We can only perform availability requests if there is a single method or a user explicitly pressed a button to allow such requests
// Otherwise we don't bother them with the automatic requests as there might be methods that do not need any requirements at all
return allDiscoveryMethods.methods.size == 1 || usedAllowedRequirementAvailabilityRequests
}
private fun updateDiscoveryRequirementAvailabilityByEvent(
availability: DiscoveryRequirementAvailability,
event: SelectLedgerEvent
): DiscoveryRequirementAvailability? {
return when (event) {
is SelectLedgerEvent.DiscoveryRequirementMissing -> availability.missRequirement(event.requirement)
is SelectLedgerEvent.DiscoveryRequirementSatisfied -> availability.satisfyRequirement(event.requirement)
is SelectLedgerEvent.PermissionsGranted -> availability.grantPermissions()
else -> null
}
}
context(Transition<SelectLedgerState, SideEffect>)
private suspend fun startDiscovery(methods: Set<DiscoveryMethod>) {
if (methods.isNotEmpty()) {
emitSideEffect(SideEffect.StartDiscovery(methods))
}
}
context(Transition<SelectLedgerState, SideEffect>)
private suspend fun stopDiscovery(methods: Set<DiscoveryMethod>) {
if (methods.isNotEmpty()) {
emitSideEffect(SideEffect.StopDiscovery(methods))
}
}
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.view
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import io.novafoundation.nova.common.utils.WithContextExtensions
import io.novafoundation.nova.common.utils.setDrawableEnd
import io.novafoundation.nova.common.utils.setTextColorRes
import io.novafoundation.nova.common.utils.updatePadding
import io.novafoundation.nova.feature_ledger_impl.R
class ItemLedgerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr), WithContextExtensions by WithContextExtensions(context) {
init {
setTextAppearance(R.style.TextAppearance_NovaFoundation_Regular_SubHeadline)
setTextColorRes(R.color.text_primary)
setDrawableEnd(R.drawable.ic_chevron_right, widthInDp = 24, paddingInDp = 4, tint = R.color.icon_secondary)
updatePadding(
top = 14.dp,
bottom = 14.dp,
start = 12.dp,
end = 12.dp
)
}
}
@@ -0,0 +1,93 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.start
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.list.instruction.InstructionItem
import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents
import io.novafoundation.nova.common.utils.formatting.spannable.highlightedText
import io.novafoundation.nova.common.utils.setupWithViewPager2
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.databinding.FragmentImportLedgerStartBinding
private const val BLUETOOTH_PAGE_INDEX = 0
private const val USB_PAGE_INDEX = 1
abstract class StartImportLedgerFragment<VM : StartImportLedgerViewModel> :
BaseFragment<VM, FragmentImportLedgerStartBinding>(),
StartImportLedgerPagerAdapter.Handler {
protected val pageAdapter by lazy(LazyThreadSafetyMode.NONE) { StartImportLedgerPagerAdapter(createPages(), this) }
override fun createBinding() = FragmentImportLedgerStartBinding.inflate(layoutInflater)
override fun initViews() {
binder.startImportLedgerToolbar.setHomeButtonListener { viewModel.backClicked() }
binder.startImportLedgerContinue.setOnClickListener {
when (binder.startImportLedgerConnectionModePages.currentItem) {
BLUETOOTH_PAGE_INDEX -> viewModel.continueWithBluetooth()
USB_PAGE_INDEX -> viewModel.continueWithUsb()
}
}
binder.startImportLedgerConnectionModePages.adapter = pageAdapter
binder.startImportLedgerConnectionMode.setupWithViewPager2(binder.startImportLedgerConnectionModePages, pageAdapter::getPageTitle)
}
override fun subscribe(viewModel: VM) {
observeBrowserEvents(viewModel)
}
override fun guideLinkClicked() {
viewModel.guideClicked()
}
private fun createPages(): List<ConnectionModePageModel> {
return buildList {
add(BLUETOOTH_PAGE_INDEX, createBluetoothPage())
add(USB_PAGE_INDEX, createUSBPage())
}
}
private fun createBluetoothPage(): ConnectionModePageModel {
return ConnectionModePageModel(
modeName = getString(R.string.start_import_ledger_connection_mode_bluetooth),
guideItems = listOf(
InstructionItem.Step(1, networkAppIsInstalledStep()),
InstructionItem.Step(2, openingNetworkAppStep()),
InstructionItem.Step(3, enableBluetoothStep()),
InstructionItem.Step(4, selectAccountStep())
)
)
}
private fun createUSBPage(): ConnectionModePageModel {
return ConnectionModePageModel(
modeName = getString(R.string.start_import_ledger_connection_mode_usb),
guideItems = listOf(
InstructionItem.Step(1, networkAppIsInstalledStep()),
InstructionItem.Step(2, openingNetworkAppStep()),
InstructionItem.Step(3, enableOTGSetting()),
InstructionItem.Step(4, selectAccountStep())
)
)
}
abstract fun networkAppIsInstalledStep(): CharSequence
abstract fun openingNetworkAppStep(): CharSequence
private fun enableBluetoothStep() = requireContext().highlightedText(
R.string.account_ledger_import_start_step_3,
R.string.account_ledger_import_start_step_3_highlighted
)
private fun enableOTGSetting() = requireContext().highlightedText(
R.string.account_ledger_import_start_step_otg,
R.string.account_ledger_import_start_step_otg_highlighted
)
private fun selectAccountStep() = requireContext().highlightedText(
R.string.account_ledger_import_start_step_4,
R.string.account_ledger_import_start_step_4_highlighted
)
}
@@ -0,0 +1,68 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.start
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import io.novafoundation.nova.common.list.instruction.InstructionAdapter
import io.novafoundation.nova.common.list.instruction.InstructionItem
import io.novafoundation.nova.common.utils.inflater
import io.novafoundation.nova.common.view.AlertModel
import io.novafoundation.nova.common.view.setModelOrHide
import io.novafoundation.nova.feature_ledger_impl.databinding.ItemImportLedgerStartPageBinding
class ConnectionModePageModel(
val modeName: String,
val guideItems: List<InstructionItem>
)
class StartImportLedgerPagerAdapter(
private val pages: List<ConnectionModePageModel>,
private val handler: Handler
) : RecyclerView.Adapter<StartImportLedgerPageViewHolder>() {
interface Handler {
fun guideLinkClicked()
}
private var alertModel: AlertModel? = null
override fun getItemCount(): Int {
return pages.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StartImportLedgerPageViewHolder {
val binder = ItemImportLedgerStartPageBinding.inflate(parent.inflater(), parent, false)
return StartImportLedgerPageViewHolder(binder, handler)
}
override fun onBindViewHolder(holder: StartImportLedgerPageViewHolder, position: Int) {
holder.bind(pages[position], alertModel)
}
fun showWarning(alertModel: AlertModel?) {
this.alertModel = alertModel
repeat(pages.size) { notifyItemChanged(it) }
}
fun getPageTitle(position: Int): CharSequence {
return pages[position].modeName
}
}
class StartImportLedgerPageViewHolder(
private val binder: ItemImportLedgerStartPageBinding,
private val handler: StartImportLedgerPagerAdapter.Handler
) : ViewHolder(binder.root) {
private val adapter = InstructionAdapter()
init {
binder.startImportLedgerInstructions.adapter = adapter
binder.startImportLedgerGuideLink.setOnClickListener { handler.guideLinkClicked() }
}
fun bind(page: ConnectionModePageModel, alertModel: AlertModel?) {
adapter.submitList(page.guideItems)
binder.startImportLedgerWarning.setModelOrHide(alertModel)
}
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.common.start
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.base.BaseViewModel
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.feature_ledger_impl.presentation.LedgerRouter
abstract class StartImportLedgerViewModel(
private val router: LedgerRouter,
private val appLinksProvider: AppLinksProvider
) : BaseViewModel(), Browserable {
override val openBrowserEvent = MutableLiveData<Event<String>>()
fun backClicked() {
router.back()
}
abstract fun continueWithBluetooth()
abstract fun continueWithUsb()
fun guideClicked() {
openBrowserEvent.value = appLinksProvider.ledgerConnectionGuide.event()
}
}
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.common.payload
import android.os.Parcelable
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerEvmAccount
import io.novasama.substrate_sdk_android.runtime.AccountId
import kotlinx.parcelize.Parcelize
@Parcelize
class LedgerGenericEvmAccountParcel(
val publicKey: ByteArray,
val accountId: AccountId,
) : Parcelable
fun LedgerEvmAccount.toParcel(): LedgerGenericEvmAccountParcel {
return LedgerGenericEvmAccountParcel(publicKey = publicKey, accountId = accountId)
}
fun LedgerGenericEvmAccountParcel.toDomain(): LedgerEvmAccount {
return LedgerEvmAccount(accountId = accountId, publicKey = publicKey)
}
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.common.payload
import android.os.Parcelable
import io.novafoundation.nova.feature_ledger_api.sdk.application.substrate.LedgerSubstrateAccount
import io.novasama.substrate_sdk_android.encrypt.EncryptionType
import kotlinx.parcelize.Parcelize
@Parcelize
class LedgerGenericSubstrateAccountParcel(
val address: String,
val publicKey: ByteArray,
val encryptionType: EncryptionType,
val derivationPath: String,
) : Parcelable
fun LedgerSubstrateAccount.toGenericParcel(): LedgerGenericSubstrateAccountParcel {
return LedgerGenericSubstrateAccountParcel(address, publicKey, encryptionType, derivationPath)
}
fun LedgerGenericSubstrateAccountParcel.toDomain(): LedgerSubstrateAccount {
return LedgerSubstrateAccount(address, publicKey, encryptionType, derivationPath)
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.finish
import android.os.Bundle
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.feature_account_api.presenatation.account.createName.CreateWalletNameFragment
import io.novafoundation.nova.feature_ledger_api.di.LedgerFeatureApi
import io.novafoundation.nova.feature_ledger_impl.di.LedgerFeatureComponent
class FinishImportGenericLedgerFragment : CreateWalletNameFragment<FinishImportGenericLedgerViewModel>() {
companion object {
private const val PAYLOAD_KEY = "FinishImportLedgerFragment.Payload"
fun getBundle(payload: FinishImportGenericLedgerPayload): Bundle {
return Bundle().apply {
putParcelable(PAYLOAD_KEY, payload)
}
}
}
override fun inject() {
FeatureUtils.getFeature<LedgerFeatureComponent>(requireContext(), LedgerFeatureApi::class.java)
.finishGenericImportLedgerComponentFactory()
.create(this, argument(PAYLOAD_KEY))
.inject(this)
}
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.finish
import android.os.Parcelable
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.common.payload.LedgerGenericEvmAccountParcel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.common.payload.LedgerGenericSubstrateAccountParcel
import kotlinx.parcelize.Parcelize
@Parcelize
class FinishImportGenericLedgerPayload(
val substrateAccount: LedgerGenericSubstrateAccountParcel,
val evmAccount: LedgerGenericEvmAccountParcel?,
) : Parcelable
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.finish
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
import io.novafoundation.nova.feature_account_api.presenatation.account.createName.CreateWalletNameViewModel
import io.novafoundation.nova.feature_ledger_impl.domain.account.connect.generic.finish.FinishImportGenericLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.common.payload.toDomain
import kotlinx.coroutines.launch
class FinishImportGenericLedgerViewModel(
private val router: LedgerRouter,
private val resourceManager: ResourceManager,
private val payload: FinishImportGenericLedgerPayload,
private val accountInteractor: AccountInteractor,
private val interactor: FinishImportGenericLedgerInteractor
) : CreateWalletNameViewModel(router, resourceManager) {
override fun proceed(name: String) {
launch {
interactor.createWallet(name, payload.substrateAccount.toDomain(), payload.evmAccount?.toDomain())
.onSuccess { continueBasedOnCodeStatus() }
.onFailure(::showError)
}
}
private suspend fun continueBasedOnCodeStatus() {
if (accountInteractor.isCodeSet()) {
router.openMain()
} else {
router.openCreatePincode()
}
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.finish.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_ledger_impl.presentation.account.connect.generic.finish.FinishImportGenericLedgerFragment
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.finish.FinishImportGenericLedgerPayload
@Subcomponent(
modules = [
FinishImportGenericLedgerModule::class
]
)
@ScreenScope
interface FinishImportGenericLedgerComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance payload: FinishImportGenericLedgerPayload,
): FinishImportGenericLedgerComponent
}
fun inject(fragment: FinishImportGenericLedgerFragment)
}
@@ -0,0 +1,58 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.finish.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.scope.ScreenScope
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.feature_account_api.data.repository.addAccount.ledger.GenericLedgerAddAccountRepository
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_ledger_impl.domain.account.connect.generic.finish.FinishImportGenericLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.domain.account.connect.generic.finish.RealFinishImportGenericLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.finish.FinishImportGenericLedgerPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.finish.FinishImportGenericLedgerViewModel
@Module(includes = [ViewModelModule::class])
class FinishImportGenericLedgerModule {
@Provides
@ScreenScope
fun provideInteractor(
genericLedgerAddAccountRepository: GenericLedgerAddAccountRepository,
accountRepository: AccountRepository,
): FinishImportGenericLedgerInteractor = RealFinishImportGenericLedgerInteractor(
genericLedgerAddAccountRepository = genericLedgerAddAccountRepository,
accountRepository = accountRepository,
)
@Provides
@IntoMap
@ViewModelKey(FinishImportGenericLedgerViewModel::class)
fun provideViewModel(
router: LedgerRouter,
resourceManager: ResourceManager,
payload: FinishImportGenericLedgerPayload,
accountInteractor: AccountInteractor,
interactor: FinishImportGenericLedgerInteractor
): ViewModel {
return FinishImportGenericLedgerViewModel(
router = router,
resourceManager = resourceManager,
payload = payload,
accountInteractor = accountInteractor,
interactor = interactor
)
}
@Provides
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): FinishImportGenericLedgerViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(FinishImportGenericLedgerViewModel::class.java)
}
}
@@ -0,0 +1,44 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.preview
import android.os.Bundle
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.feature_account_api.presenatation.account.chain.ChainAccountsAdapter
import io.novafoundation.nova.feature_account_api.presenatation.account.chain.preview.BaseChainAccountsPreviewFragment
import io.novafoundation.nova.feature_ledger_api.di.LedgerFeatureApi
import io.novafoundation.nova.feature_ledger_impl.di.LedgerFeatureComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessagePresentable
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.setupLedgerMessages
import javax.inject.Inject
class PreviewImportGenericLedgerFragment : BaseChainAccountsPreviewFragment<PreviewImportGenericLedgerViewModel>(), ChainAccountsAdapter.Handler {
@Inject
lateinit var ledgerMessagePresentable: LedgerMessagePresentable
companion object {
private const val PAYLOAD_KEY = "PreviewImportGenericLedgerFragment.Payload"
fun getBundle(payload: PreviewImportGenericLedgerPayload): Bundle {
return Bundle().apply {
putParcelable(PAYLOAD_KEY, payload)
}
}
}
override fun inject() {
FeatureUtils.getFeature<LedgerFeatureComponent>(
requireContext(),
LedgerFeatureApi::class.java
)
.previewImportGenericLedgerComponentFactory()
.create(this, argument(PAYLOAD_KEY))
.inject(this)
}
override fun subscribe(viewModel: PreviewImportGenericLedgerViewModel) {
super.subscribe(viewModel)
setupLedgerMessages(ledgerMessagePresentable)
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.preview
import android.os.Parcelable
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.common.payload.LedgerGenericEvmAccountParcel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.common.payload.LedgerGenericSubstrateAccountParcel
import kotlinx.parcelize.Parcelize
@Parcelize
class PreviewImportGenericLedgerPayload(
val accountIndex: Int,
val substrateAccount: LedgerGenericSubstrateAccountParcel,
val evmAccount: LedgerGenericEvmAccountParcel?,
val deviceId: String
) : Parcelable
@@ -0,0 +1,132 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.preview
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.address.AddressIconGenerator
import io.novafoundation.nova.common.address.format.AddressScheme
import io.novafoundation.nova.common.address.format.AddressSchemeFormatter
import io.novafoundation.nova.common.list.toListWithHeaders
import io.novafoundation.nova.common.presentation.DescriptiveButtonState
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.flowOf
import io.novafoundation.nova.feature_account_api.presenatation.account.chain.model.ChainAccountGroupUi
import io.novafoundation.nova.feature_account_api.presenatation.account.chain.preview.BaseChainAccountsPreviewViewModel
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
import io.novafoundation.nova.feature_ledger_impl.R
import io.novafoundation.nova.feature_ledger_impl.domain.account.connect.generic.preview.PreviewImportGenericLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommand
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.LedgerMessageCommands
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.createLedgerReviewAddresses
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.errors.handleLedgerError
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.finish.FinishImportGenericLedgerPayload
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novasama.substrate_sdk_android.extensions.asEthereumAccountId
import io.novasama.substrate_sdk_android.extensions.toAddress
import io.novasama.substrate_sdk_android.ss58.SS58Encoder.toAccountId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PreviewImportGenericLedgerViewModel(
private val interactor: PreviewImportGenericLedgerInteractor,
private val router: LedgerRouter,
private val iconGenerator: AddressIconGenerator,
private val payload: PreviewImportGenericLedgerPayload,
private val externalActions: ExternalActions.Presentation,
private val chainRegistry: ChainRegistry,
private val resourceManager: ResourceManager,
private val messageCommandFormatter: MessageCommandFormatter,
private val addressSchemeFormatter: AddressSchemeFormatter,
) : BaseChainAccountsPreviewViewModel(
iconGenerator = iconGenerator,
externalActions = externalActions,
chainRegistry = chainRegistry,
router = router
),
LedgerMessageCommands {
override val ledgerMessageCommands: MutableLiveData<Event<LedgerMessageCommand>> = MutableLiveData()
override val chainAccountProjections = flowOf {
interactor.availableChainAccounts(
substrateAccountId = payload.substrateAccount.address.toAccountId(),
evmAccountId = payload.evmAccount?.accountId
).toListWithHeaders(
keyMapper = { scheme, _ -> formatGroupHeader(scheme) },
valueMapper = { account -> mapChainAccountPreviewToUi(account) }
)
}
.shareInBackground()
override val buttonState: Flow<DescriptiveButtonState> = flowOf {
DescriptiveButtonState.Enabled(resourceManager.getString(R.string.common_continue))
}
val device = flowOf {
interactor.getDevice(payload.deviceId)
}
private var verifyAddressJob: Job? = null
override fun continueClicked() {
verifyAddressJob?.cancel()
verifyAddressJob = launch {
verifyAccount()
}
}
private fun formatGroupHeader(addressScheme: AddressScheme): ChainAccountGroupUi {
return ChainAccountGroupUi(
id = addressScheme.name,
title = addressSchemeFormatter.accountsLabel(addressScheme),
action = null
)
}
private suspend fun verifyAccount() {
val device = device.first()
ledgerMessageCommands.value = messageCommandFormatter.reviewAddressCommand(
addresses = createLedgerReviewAddresses(
allowedAddressSchemes = AddressScheme.entries,
AddressScheme.SUBSTRATE to payload.substrateAccount.address,
AddressScheme.EVM to payload.evmAccount?.accountId?.asEthereumAccountId()?.toAddress()?.value
),
device = device,
onCancel = ::verifyAddressCancelled,
).event()
val result = withContext(Dispatchers.Default) {
interactor.verifyAddressOnLedger(payload.accountIndex, payload.deviceId)
}
result.onFailure {
handleLedgerError(
reason = it,
device = device,
commandFormatter = messageCommandFormatter,
onRetry = ::continueClicked
)
}.onSuccess {
ledgerMessageCommands.value = messageCommandFormatter.hideCommand().event()
onAccountVerified()
}
}
private fun onAccountVerified() {
val nextPayload = FinishImportGenericLedgerPayload(payload.substrateAccount, payload.evmAccount)
router.openFinishImportLedgerGeneric(nextPayload)
}
private fun verifyAddressCancelled() {
ledgerMessageCommands.value = messageCommandFormatter.hideCommand().event()
verifyAddressJob?.cancel()
verifyAddressJob = null
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.preview.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_ledger_impl.presentation.account.connect.generic.preview.PreviewImportGenericLedgerFragment
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.preview.PreviewImportGenericLedgerPayload
@Subcomponent(
modules = [
PreviewImportGenericLedgerModule::class
]
)
@ScreenScope
interface PreviewImportGenericLedgerComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance payload: PreviewImportGenericLedgerPayload
): PreviewImportGenericLedgerComponent
}
fun inject(fragment: PreviewImportGenericLedgerFragment)
}
@@ -0,0 +1,75 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.preview.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.address.AddressIconGenerator
import io.novafoundation.nova.common.address.format.AddressSchemeFormatter
import io.novafoundation.nova.common.di.scope.ScreenScope
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.feature_account_api.presenatation.actions.ExternalActions
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_core.domain.LedgerMigrationTracker
import io.novafoundation.nova.feature_ledger_impl.di.annotations.GenericLedger
import io.novafoundation.nova.feature_ledger_impl.domain.account.connect.generic.preview.PreviewImportGenericLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.domain.account.connect.generic.preview.RealPreviewImportGenericLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.preview.PreviewImportGenericLedgerPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.preview.PreviewImportGenericLedgerViewModel
import io.novafoundation.nova.feature_ledger_impl.sdk.application.substrate.newApp.GenericSubstrateLedgerApplication
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
@Module(includes = [ViewModelModule::class])
class PreviewImportGenericLedgerModule {
@Provides
@ScreenScope
fun provideInteractor(
ledgerMigrationTracker: LedgerMigrationTracker,
genericSubstrateLedgerApplication: GenericSubstrateLedgerApplication,
ledgerDiscoveryService: LedgerDeviceDiscoveryService
): PreviewImportGenericLedgerInteractor {
return RealPreviewImportGenericLedgerInteractor(ledgerMigrationTracker, genericSubstrateLedgerApplication, ledgerDiscoveryService)
}
@Provides
@IntoMap
@ViewModelKey(PreviewImportGenericLedgerViewModel::class)
fun provideViewModel(
interactor: PreviewImportGenericLedgerInteractor,
router: LedgerRouter,
iconGenerator: AddressIconGenerator,
payload: PreviewImportGenericLedgerPayload,
externalActions: ExternalActions.Presentation,
chainRegistry: ChainRegistry,
resourceManager: ResourceManager,
@GenericLedger messageCommandFormatter: MessageCommandFormatter,
addressSchemeFormatter: AddressSchemeFormatter
): ViewModel {
return PreviewImportGenericLedgerViewModel(
interactor = interactor,
router = router,
iconGenerator = iconGenerator,
payload = payload,
externalActions = externalActions,
chainRegistry = chainRegistry,
resourceManager = resourceManager,
messageCommandFormatter = messageCommandFormatter,
addressSchemeFormatter = addressSchemeFormatter
)
}
@Provides
fun provideViewModelCreator(
fragment: Fragment,
viewModelFactory: ViewModelProvider.Factory
): PreviewImportGenericLedgerViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(PreviewImportGenericLedgerViewModel::class.java)
}
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectAddress
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.utils.makeGone
import io.novafoundation.nova.feature_ledger_api.di.LedgerFeatureApi
import io.novafoundation.nova.feature_ledger_impl.di.LedgerFeatureComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectAddressLedgerFragment
class SelectAddressImportGenericLedgerFragment : SelectAddressLedgerFragment<SelectAddressImportGenericLedgerViewModel>() {
override fun initViews() {
super.initViews()
binder.ledgerSelectAddressChain.makeGone()
}
override fun inject() {
FeatureUtils.getFeature<LedgerFeatureComponent>(requireContext(), LedgerFeatureApi::class.java)
.selectAddressImportLedgerGenericComponentFactory()
.create(this, payload())
.inject(this)
}
}
@@ -0,0 +1,78 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectAddress
import io.novafoundation.nova.common.address.AddressIconGenerator
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.view.AlertModel
import io.novafoundation.nova.feature_account_api.domain.model.LedgerVariant
import io.novafoundation.nova.feature_account_api.presenatation.addressActions.AddressActionsMixin
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.LedgerAccount
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.SelectAddressLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.generic.GenericLedgerEvmAlertFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectAddressLedgerViewModel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectLedgerAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.model.AddressVerificationMode
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.common.payload.toGenericParcel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.common.payload.toParcel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.preview.PreviewImportGenericLedgerPayload
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class SelectAddressImportGenericLedgerViewModel(
private val router: LedgerRouter,
private val payload: SelectLedgerAddressPayload,
interactor: SelectAddressLedgerInteractor,
addressIconGenerator: AddressIconGenerator,
private val resourceManager: ResourceManager,
private val evmUpdateFormatter: GenericLedgerEvmAlertFormatter,
chainRegistry: ChainRegistry,
messageCommandFormatter: MessageCommandFormatter,
addressActionsMixinFactory: AddressActionsMixin.Factory
) : SelectAddressLedgerViewModel(
router = router,
interactor = interactor,
addressIconGenerator = addressIconGenerator,
resourceManager = resourceManager,
payload = payload,
chainRegistry = chainRegistry,
messageCommandFormatter = messageCommandFormatter,
addressActionsMixinFactory = addressActionsMixinFactory
) {
override val ledgerVariant: LedgerVariant = LedgerVariant.GENERIC
override val addressVerificationMode = AddressVerificationMode.Disabled
init {
loadedAccounts.onEach { accounts ->
val needsUpdateToSupportEvm = accounts.any { it.evm == null }
val model = createAlertModel(needsUpdateToSupportEvm)
_alertFlow.emit(model)
}
.launchIn(this)
}
override fun onAccountVerified(account: LedgerAccount) {
launch {
val payload = PreviewImportGenericLedgerPayload(
accountIndex = account.index,
substrateAccount = account.substrate.toGenericParcel(),
evmAccount = account.evm?.toParcel(),
deviceId = payload.deviceId
)
router.openPreviewLedgerAccountsGeneric(payload)
}
}
private fun createAlertModel(needsUpdateToSupportEvm: Boolean): AlertModel? {
return if (needsUpdateToSupportEvm) {
evmUpdateFormatter.createUpdateAppToGetEvmAddressAlert()
} else {
null
}
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectAddress.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_ledger_impl.presentation.account.common.selectAddress.SelectLedgerAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectAddress.SelectAddressImportGenericLedgerFragment
@Subcomponent(
modules = [
SelectAddressImportGenericLedgerModule::class
]
)
@ScreenScope
interface SelectAddressImportGenericLedgerComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance payload: SelectLedgerAddressPayload,
): SelectAddressImportGenericLedgerComponent
}
fun inject(fragment: SelectAddressImportGenericLedgerFragment)
}
@@ -0,0 +1,57 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectAddress.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.address.AddressIconGenerator
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.feature_account_api.presenatation.addressActions.AddressActionsMixin
import io.novafoundation.nova.feature_ledger_impl.di.annotations.GenericLedger
import io.novafoundation.nova.feature_ledger_impl.domain.account.common.selectAddress.SelectAddressLedgerInteractor
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.generic.GenericLedgerEvmAlertFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectLedgerAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectAddress.SelectAddressImportGenericLedgerViewModel
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
@Module(includes = [ViewModelModule::class])
class SelectAddressImportGenericLedgerModule {
@Provides
@IntoMap
@ViewModelKey(SelectAddressImportGenericLedgerViewModel::class)
fun provideViewModel(
router: LedgerRouter,
interactor: SelectAddressLedgerInteractor,
addressIconGenerator: AddressIconGenerator,
resourceManager: ResourceManager,
payload: SelectLedgerAddressPayload,
chainRegistry: ChainRegistry,
@GenericLedger messageCommandFormatter: MessageCommandFormatter,
evmAlertFormatter: GenericLedgerEvmAlertFormatter,
addressActionsMixinFactory: AddressActionsMixin.Factory
): ViewModel {
return SelectAddressImportGenericLedgerViewModel(
router = router,
interactor = interactor,
addressIconGenerator = addressIconGenerator,
resourceManager = resourceManager,
payload = payload,
chainRegistry = chainRegistry,
messageCommandFormatter = messageCommandFormatter,
evmUpdateFormatter = evmAlertFormatter,
addressActionsMixinFactory = addressActionsMixinFactory
)
}
@Provides
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): SelectAddressImportGenericLedgerViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(SelectAddressImportGenericLedgerViewModel::class.java)
}
}
@@ -0,0 +1,25 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectLedger
import android.os.Bundle
import androidx.core.os.bundleOf
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.feature_ledger_api.di.LedgerFeatureApi
import io.novafoundation.nova.feature_ledger_impl.di.LedgerFeatureComponent
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerFragment
class SelectLedgerGenericImportFragment : SelectLedgerFragment<SelectLedgerGenericImportViewModel>() {
companion object {
private const val PAYLOAD_KEY = "SelectLedgerGenericImportFragment.PAYLOAD_KEY"
fun getBundle(payload: SelectLedgerGenericPayload): Bundle = bundleOf(PAYLOAD_KEY to payload)
}
override fun inject() {
FeatureUtils.getFeature<LedgerFeatureComponent>(requireContext(), LedgerFeatureApi::class.java)
.selectLedgerGenericImportComponentFactory()
.create(this, argument(PAYLOAD_KEY))
.inject(this)
}
}
@@ -0,0 +1,55 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectLedger
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.bluetooth.BluetoothManager
import io.novafoundation.nova.common.utils.event
import io.novafoundation.nova.common.utils.location.LocationManager
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.feature_ledger_api.sdk.device.LedgerDevice
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers.LedgerDeviceFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectAddress.SelectLedgerAddressPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerPayload
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerViewModel
import io.novafoundation.nova.runtime.ext.ChainGeneses
class SelectLedgerGenericImportViewModel(
private val router: LedgerRouter,
private val messageCommandFormatter: MessageCommandFormatter,
discoveryService: LedgerDeviceDiscoveryService,
permissionsAsker: PermissionsAsker.Presentation,
bluetoothManager: BluetoothManager,
locationManager: LocationManager,
resourceManager: ResourceManager,
messageFormatter: LedgerMessageFormatter,
payload: SelectLedgerPayload,
deviceMapperFactory: LedgerDeviceFormatter,
) : SelectLedgerViewModel(
discoveryService = discoveryService,
permissionsAsker = permissionsAsker,
bluetoothManager = bluetoothManager,
locationManager = locationManager,
router = router,
resourceManager = resourceManager,
messageFormatter = messageFormatter,
ledgerDeviceFormatter = deviceMapperFactory,
messageCommandFormatter = messageCommandFormatter,
payload = payload
) {
override suspend fun verifyConnection(device: LedgerDevice) {
ledgerMessageCommands.value = messageCommandFormatter.hideCommand().event()
val payload = SelectLedgerAddressPayload(
deviceId = device.id,
substrateChainId = getPreviewBalanceChainId()
)
router.openSelectAddressGenericLedger(payload)
}
private fun getPreviewBalanceChainId() = ChainGeneses.POLKADOT
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectLedger
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.selectLedger.SelectLedgerPayload
import kotlinx.android.parcel.Parcelize
@Parcelize
class SelectLedgerGenericPayload(override val connectionMode: SelectLedgerPayload.ConnectionMode) : SelectLedgerPayload
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectLedger.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_ledger_impl.presentation.account.connect.generic.selectLedger.SelectLedgerGenericImportFragment
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectLedger.SelectLedgerGenericPayload
@Subcomponent(
modules = [
SelectLedgerGenericImportModule::class
]
)
@ScreenScope
interface SelectLedgerGenericImportComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance payload: SelectLedgerGenericPayload,
): SelectLedgerGenericImportComponent
}
fun inject(fragment: SelectLedgerGenericImportFragment)
}
@@ -0,0 +1,61 @@
package io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectLedger.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.modules.shared.PermissionAskerForFragmentModule
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.bluetooth.BluetoothManager
import io.novafoundation.nova.common.utils.location.LocationManager
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.feature_ledger_api.sdk.discovery.LedgerDeviceDiscoveryService
import io.novafoundation.nova.feature_ledger_impl.di.annotations.GenericLedger
import io.novafoundation.nova.feature_ledger_impl.presentation.LedgerRouter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.MessageCommandFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.bottomSheet.mappers.LedgerDeviceFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.common.formatters.LedgerMessageFormatter
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectLedger.SelectLedgerGenericImportViewModel
import io.novafoundation.nova.feature_ledger_impl.presentation.account.connect.generic.selectLedger.SelectLedgerGenericPayload
@Module(includes = [ViewModelModule::class, PermissionAskerForFragmentModule::class])
class SelectLedgerGenericImportModule {
@Provides
@IntoMap
@ViewModelKey(SelectLedgerGenericImportViewModel::class)
fun provideViewModel(
discoveryService: LedgerDeviceDiscoveryService,
permissionsAsker: PermissionsAsker.Presentation,
bluetoothManager: BluetoothManager,
locationManager: LocationManager,
router: LedgerRouter,
resourceManager: ResourceManager,
@GenericLedger messageFormatter: LedgerMessageFormatter,
payload: SelectLedgerGenericPayload,
deviceMapperFactory: LedgerDeviceFormatter,
@GenericLedger messageCommandFormatter: MessageCommandFormatter
): ViewModel {
return SelectLedgerGenericImportViewModel(
discoveryService = discoveryService,
permissionsAsker = permissionsAsker,
bluetoothManager = bluetoothManager,
locationManager = locationManager,
router = router,
resourceManager = resourceManager,
messageFormatter = messageFormatter,
deviceMapperFactory = deviceMapperFactory,
messageCommandFormatter = messageCommandFormatter,
payload = payload
)
}
@Provides
fun provideViewModelCreator(fragment: Fragment, viewModelFactory: ViewModelProvider.Factory): SelectLedgerGenericImportViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(SelectLedgerGenericImportViewModel::class.java)
}
}

Some files were not shown because too many files have changed in this diff Show More