Initial commit: Pezkuwi Wallet Android

Complete rebrand of Nova Wallet for Pezkuwichain ecosystem.

## Features
- Full Pezkuwichain support (HEZ & PEZ tokens)
- Polkadot ecosystem compatibility
- Staking, Governance, DeFi, NFTs
- XCM cross-chain transfers
- Hardware wallet support (Ledger, Polkadot Vault)
- WalletConnect v2
- Push notifications

## Languages
- English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese

Based on Nova Wallet by Novasama Technologies GmbH
© Dijital Kurdistan Tech Institute 2026
This commit is contained in:
2026-01-23 01:31:12 +03:00
commit 31c8c5995f
7621 changed files with 425838 additions and 0 deletions
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,10 @@
package io.novafoundation.nova.feature_ahm_impl.data.config
import io.novafoundation.nova.feature_ahm_impl.BuildConfig
import retrofit2.http.GET
interface ChainMigrationConfigApi {
@GET(BuildConfig.AHM_CONFIG_URL)
suspend fun getConfig(): List<ChainMigrationConfigRemote>
}
@@ -0,0 +1,47 @@
package io.novafoundation.nova.feature_ahm_impl.data.config
import io.novafoundation.nova.feature_ahm_api.domain.model.ChainMigrationConfig
import java.math.BigInteger
import java.util.Date
import kotlin.time.Duration.Companion.seconds
class ChainMigrationConfigRemote(
val sourceData: ChainData,
val destinationData: ChainData,
val blockNumber: BigInteger,
val timestamp: Long,
val newTokenNames: List<String>,
val bannerPath: String,
val migrationInProgress: Boolean,
val wikiURL: String
) {
class ChainData(
val chainId: String,
val assetId: Int,
val minBalance: BigInteger,
val averageFee: BigInteger
)
}
fun ChainMigrationConfigRemote.toDomain(): ChainMigrationConfig {
return ChainMigrationConfig(
originData = sourceData.toDomain(),
destinationData = destinationData.toDomain(),
blockNumberStartAt = blockNumber,
timeStartAt = Date(timestamp.seconds.inWholeMilliseconds),
newTokenNames = newTokenNames,
bannerPath = bannerPath,
migrationInProgress = migrationInProgress,
wikiURL = wikiURL
)
}
fun ChainMigrationConfigRemote.ChainData.toDomain(): ChainMigrationConfig.ChainData {
return ChainMigrationConfig.ChainData(
chainId = chainId,
assetId = assetId,
minBalance = minBalance,
averageFee = averageFee
)
}
@@ -0,0 +1,55 @@
package io.novafoundation.nova.feature_ahm_impl.data.repository
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.domain.balance.totalBalance
import io.novafoundation.nova.common.utils.mapToSet
import io.novafoundation.nova.core_db.dao.AssetDao
import io.novafoundation.nova.core_db.model.AssetLocal
import io.novafoundation.nova.feature_ahm_api.data.repository.ChainMigrationRepository
import io.novafoundation.nova.runtime.ext.UTILITY_ASSET_ID
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
import io.novasama.substrate_sdk_android.hash.isPositive
private const val CHAINS_WITH_ASSET_BALANCE = "CHAINS_WITH_ASSET_BALANCE"
private const val CHAIN_MIGRATION_INFO_SHOWN_PREFIX = "CHAIN_MIGRATION_INFO_SHOWN_PREFIX_"
class RealChainMigrationRepository(
private val assetDao: AssetDao,
private val preferences: Preferences
) : ChainMigrationRepository {
override suspend fun cacheBalancesForChainMigrationDetection() {
val chainsWithAssetBalance = assetDao.getAssetsById(id = UTILITY_ASSET_ID)
.filter { it.hasBalance() }
.mapToSet { it.chainId }
saveToStorage(chainsWithAssetBalance)
}
override fun isMigrationDetailsWasShown(chainId: String): Boolean {
return preferences.getBoolean(getChainInfoShownKey(chainId), false)
}
override fun isChainMigrationDetailsNeeded(chainId: String): Boolean {
return chainId in getChainsWithAssetBalance()
}
override suspend fun setInfoShownForChain(chainId: String) {
preferences.putBoolean(getChainInfoShownKey(chainId), true)
}
private fun AssetLocal.hasBalance(): Boolean {
return totalBalance(freeInPlanks, reservedInPlanks).isPositive()
}
private fun saveToStorage(chainsWithAssetBalance: Set<String>) {
val currentChains = getChainsWithAssetBalance()
setChainsWithAssetBalance(currentChains + chainsWithAssetBalance)
}
private fun getChainsWithAssetBalance() = preferences.getStringSet(CHAINS_WITH_ASSET_BALANCE)
private fun setChainsWithAssetBalance(chains: Set<ChainId>) = preferences.putStringSet(CHAINS_WITH_ASSET_BALANCE, chains)
private fun getChainInfoShownKey(chainId: String) = CHAIN_MIGRATION_INFO_SHOWN_PREFIX + chainId
}
@@ -0,0 +1,37 @@
package io.novafoundation.nova.feature_ahm_impl.data.repository
import io.novafoundation.nova.common.data.memory.SingleValueCache
import io.novafoundation.nova.feature_ahm_api.data.repository.MigrationInfoRepository
import io.novafoundation.nova.feature_ahm_api.domain.model.ChainMigrationConfig
import io.novafoundation.nova.feature_ahm_impl.data.config.ChainMigrationConfigApi
import io.novafoundation.nova.feature_ahm_impl.data.config.toDomain
class RealMigrationInfoRepository(
private val api: ChainMigrationConfigApi
) : MigrationInfoRepository {
private val config = SingleValueCache {
val configResponse = api.getConfig()
configResponse.map { it.toDomain() }
}
override suspend fun getConfigByOriginChain(chainId: String): ChainMigrationConfig? {
return getConfigsInternal().getOrNull()?.firstOrNull { it.originData.chainId == chainId }
}
override suspend fun getConfigByDestinationChain(chainId: String): ChainMigrationConfig? {
return getConfigsInternal().getOrNull()?.firstOrNull { it.destinationData.chainId == chainId }
}
override suspend fun getAllConfigs(): List<ChainMigrationConfig> {
return getConfigsInternal().getOrNull() ?: emptyList()
}
override suspend fun loadConfigs() {
getConfigsInternal()
}
private suspend fun getConfigsInternal() = runCatching {
config()
}
}
@@ -0,0 +1,47 @@
package io.novafoundation.nova.feature_ahm_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_ahm_api.di.ChainMigrationFeatureApi
import io.novafoundation.nova.feature_ahm_impl.presentation.ChainMigrationRouter
import io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails.di.ChainMigrationDetailsComponent
import io.novafoundation.nova.feature_banners_api.di.BannersFeatureApi
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.runtime.di.RuntimeApi
@Component(
dependencies = [
ChainMigrationFeatureDependencies::class,
],
modules = [
ChainMigrationFeatureModule::class,
]
)
@FeatureScope
interface ChainMigrationFeatureComponent : ChainMigrationFeatureApi {
fun chainMigrationDetailsComponentFactory(): ChainMigrationDetailsComponent.Factory
@Component.Factory
interface Factory {
fun create(
@BindsInstance router: ChainMigrationRouter,
deps: ChainMigrationFeatureDependencies
): ChainMigrationFeatureComponent
}
@Component(
dependencies = [
CommonApi::class,
DbApi::class,
RuntimeApi::class,
BannersFeatureApi::class,
WalletFeatureApi::class
]
)
interface AccountFeatureDependenciesComponent : ChainMigrationFeatureDependencies
}
@@ -0,0 +1,44 @@
package io.novafoundation.nova.feature_ahm_impl.di
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.data.repository.ToggleFeatureRepository
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
import io.novafoundation.nova.core_db.dao.AssetDao
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannersMixinFactory
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSourceFactory
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.TokenFormatter
import io.novafoundation.nova.runtime.di.REMOTE_STORAGE_SOURCE
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import io.novafoundation.nova.runtime.storage.source.StorageDataSource
import javax.inject.Named
interface ChainMigrationFeatureDependencies {
val resourceManager: ResourceManager
val promotionBannersMixinFactory: PromotionBannersMixinFactory
val bannersSourceFactory: BannersSourceFactory
val assetDao: AssetDao
val preferences: Preferences
val chainRegistry: ChainRegistry
val apiCreator: NetworkApiCreator
val automaticInteractionGate: AutomaticInteractionGate
val toggleFeatureRepository: ToggleFeatureRepository
val chainStateRepository: ChainStateRepository
val tokenFormatter: TokenFormatter
@Named(REMOTE_STORAGE_SOURCE)
fun remoteStorageSource(): StorageDataSource
}
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_ahm_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_ahm_impl.presentation.ChainMigrationRouter
import io.novafoundation.nova.feature_banners_api.di.BannersFeatureApi
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
import io.novafoundation.nova.runtime.di.RuntimeApi
import javax.inject.Inject
@ApplicationScope
class ChainMigrationFeatureHolder @Inject constructor(
featureContainer: FeatureContainer,
private val router: ChainMigrationRouter,
) : FeatureApiHolder(featureContainer) {
override fun initializeDependencies(): Any {
val accountFeatureDependencies = DaggerChainMigrationFeatureComponent_AccountFeatureDependenciesComponent.builder()
.commonApi(commonApi())
.dbApi(getFeature(DbApi::class.java))
.runtimeApi(getFeature(RuntimeApi::class.java))
.bannersFeatureApi(getFeature(BannersFeatureApi::class.java))
.walletFeatureApi(getFeature(WalletFeatureApi::class.java))
.build()
return DaggerChainMigrationFeatureComponent.factory()
.create(
router = router,
deps = accountFeatureDependencies
)
}
}
@@ -0,0 +1,100 @@
package io.novafoundation.nova.feature_ahm_impl.di
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.data.repository.ToggleFeatureRepository
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.core_db.dao.AssetDao
import io.novafoundation.nova.feature_ahm_api.data.repository.ChainMigrationRepository
import io.novafoundation.nova.feature_ahm_api.data.repository.MigrationInfoRepository
import io.novafoundation.nova.feature_ahm_api.domain.ChainMigrationDetailsSelectToShowUseCase
import io.novafoundation.nova.feature_ahm_impl.data.config.ChainMigrationConfigApi
import io.novafoundation.nova.feature_ahm_impl.data.repository.RealChainMigrationRepository
import io.novafoundation.nova.feature_ahm_impl.data.repository.RealMigrationInfoRepository
import io.novafoundation.nova.feature_ahm_impl.di.modules.DeepLinkModule
import io.novafoundation.nova.feature_ahm_api.domain.ChainMigrationInfoUseCase
import io.novafoundation.nova.feature_ahm_impl.domain.ChainMigrationDetailsInteractor
import io.novafoundation.nova.feature_ahm_impl.domain.RealChainMigrationInfoUseCase
import io.novafoundation.nova.feature_ahm_impl.domain.RealChainMigrationDetailsSelectToShowUseCase
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.repository.ChainStateRepository
@Module(
includes = [DeepLinkModule::class]
)
class ChainMigrationFeatureModule {
@Provides
@FeatureScope
fun provideChainMigrationConfigApi(
apiCreator: NetworkApiCreator
): ChainMigrationConfigApi {
return apiCreator.create(ChainMigrationConfigApi::class.java)
}
@Provides
@FeatureScope
fun provideChainMigrationRepository(
assetDao: AssetDao,
preferences: Preferences,
): ChainMigrationRepository {
return RealChainMigrationRepository(
assetDao,
preferences
)
}
@Provides
@FeatureScope
fun provideMigrationInfoRepository(
api: ChainMigrationConfigApi
): MigrationInfoRepository {
return RealMigrationInfoRepository(api)
}
@Provides
@FeatureScope
fun provideChainMigrationDetailsInteractor(
chainRegistry: ChainRegistry,
chainMigrationRepository: ChainMigrationRepository,
migrationInfoRepository: MigrationInfoRepository
): ChainMigrationDetailsInteractor {
return ChainMigrationDetailsInteractor(
chainRegistry,
chainMigrationRepository,
migrationInfoRepository
)
}
@Provides
@FeatureScope
fun provideAssetMigrationUseCase(
migrationInfoRepository: MigrationInfoRepository,
toggleFeatureRepository: ToggleFeatureRepository,
chainRegistry: ChainRegistry,
chainStateRepository: ChainStateRepository
): ChainMigrationInfoUseCase {
return RealChainMigrationInfoUseCase(
migrationInfoRepository,
toggleFeatureRepository,
chainRegistry,
chainStateRepository
)
}
@Provides
@FeatureScope
fun provideChainMigrationDetailsSelectToShowUseCase(
chainMigrationRepository: ChainMigrationRepository,
migrationInfoRepository: MigrationInfoRepository,
chainStateRepository: ChainStateRepository
): ChainMigrationDetailsSelectToShowUseCase {
return RealChainMigrationDetailsSelectToShowUseCase(
migrationInfoRepository,
chainMigrationRepository,
chainStateRepository
)
}
}
@@ -0,0 +1,29 @@
package io.novafoundation.nova.feature_ahm_impl.di.modules
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
import io.novafoundation.nova.feature_ahm_api.di.deeplinks.ChainMigrationDeepLinks
import io.novafoundation.nova.feature_ahm_impl.presentation.ChainMigrationRouter
import io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails.deeplink.ChainMigrationDetailsDeepLinkHandler
@Module
class DeepLinkModule {
@Provides
@FeatureScope
fun provideStakingDashboardDeepLinkHandler(
router: ChainMigrationRouter,
automaticInteractionGate: AutomaticInteractionGate
) = ChainMigrationDetailsDeepLinkHandler(
router,
automaticInteractionGate
)
@Provides
@FeatureScope
fun provideDeepLinks(stakingDashboard: ChainMigrationDetailsDeepLinkHandler): ChainMigrationDeepLinks {
return ChainMigrationDeepLinks(listOf(stakingDashboard))
}
}
@@ -0,0 +1,32 @@
package io.novafoundation.nova.feature_ahm_impl.domain
import io.novafoundation.nova.feature_ahm_api.data.repository.ChainMigrationRepository
import io.novafoundation.nova.feature_ahm_api.data.repository.MigrationInfoRepository
import io.novafoundation.nova.feature_ahm_api.domain.model.ChainMigrationConfig
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import io.novafoundation.nova.runtime.multiNetwork.chainFlow
import kotlinx.coroutines.flow.Flow
class ChainMigrationDetailsInteractor(
private val chainRegistry: ChainRegistry,
private val chainMigrationRepository: ChainMigrationRepository,
private val migrationInfoRepository: MigrationInfoRepository
) {
fun chainFlow(chainId: String): Flow<Chain> {
return chainRegistry.chainFlow(chainId)
}
suspend fun getChain(chainId: String): Chain {
return chainRegistry.getChain(chainId)
}
suspend fun getChainMigrationConfig(chainId: String): ChainMigrationConfig? {
return migrationInfoRepository.getConfigByOriginChain(chainId)
}
suspend fun markMigrationInfoAlreadyShown(chainId: String) {
chainMigrationRepository.setInfoShownForChain(chainId)
}
}
@@ -0,0 +1,24 @@
package io.novafoundation.nova.feature_ahm_impl.domain
import io.novafoundation.nova.feature_ahm_api.data.repository.ChainMigrationRepository
import io.novafoundation.nova.feature_ahm_api.data.repository.MigrationInfoRepository
import io.novafoundation.nova.feature_ahm_api.domain.ChainMigrationDetailsSelectToShowUseCase
import io.novafoundation.nova.runtime.repository.ChainStateRepository
class RealChainMigrationDetailsSelectToShowUseCase(
private val migrationInfoRepository: MigrationInfoRepository,
private val chainMigrationRepository: ChainMigrationRepository,
private val chainStateRepository: ChainStateRepository,
) : ChainMigrationDetailsSelectToShowUseCase {
override suspend fun getChainIdsToShowMigrationDetails(): List<String> {
val configs = migrationInfoRepository.getAllConfigs()
return configs
.filter {
val detailsWasNotShown = !chainMigrationRepository.isMigrationDetailsWasShown(it.originData.chainId)
val chainRequireMigrationDetails = chainMigrationRepository.isChainMigrationDetailsNeeded(it.originData.chainId)
detailsWasNotShown && chainRequireMigrationDetails && chainStateRepository.isMigrationBlockPassed(it)
}
.map { it.originData.chainId }
}
}
@@ -0,0 +1,54 @@
package io.novafoundation.nova.feature_ahm_impl.domain
import io.novafoundation.nova.common.data.repository.ToggleFeatureRepository
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.feature_ahm_api.data.repository.MigrationInfoRepository
import io.novafoundation.nova.feature_ahm_api.domain.ChainMigrationInfoUseCase
import io.novafoundation.nova.feature_ahm_api.domain.model.ChainMigrationConfigWithChains
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
import io.novafoundation.nova.runtime.repository.ChainStateRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
class RealChainMigrationInfoUseCase(
private val migrationInfoRepository: MigrationInfoRepository,
private val toggleFeatureRepository: ToggleFeatureRepository,
private val chainRegistry: ChainRegistry,
private val chainStateRepository: ChainStateRepository
) : ChainMigrationInfoUseCase {
override fun observeMigrationConfigOrNull(chainId: String, assetId: Int): Flow<ChainMigrationConfigWithChains?> = flowOfAll {
val config = migrationInfoRepository.getConfigByOriginChain(chainId)
?: migrationInfoRepository.getConfigByDestinationChain(chainId)
?: return@flowOfAll emptyFlow()
if (chainStateRepository.isMigrationBlockNotPassed(config)) return@flowOfAll emptyFlow()
chainRegistry.chainsById
.map {
val sourceChain = it.getValue(config.originData.chainId)
val destinationChain = it.getValue(config.destinationData.chainId)
ChainMigrationConfigWithChains(
config = config,
originChain = sourceChain,
originAsset = sourceChain.assetsById.getValue(config.originData.assetId),
destinationChain = destinationChain,
destinationAsset = destinationChain.assetsById.getValue(config.destinationData.assetId)
)
}
}
override fun observeInfoShouldBeHidden(key: String, chainId: String, assetId: Int): Flow<Boolean> {
return toggleFeatureRepository.observe(getKeyFor(key, chainId, assetId))
}
override fun markMigrationInfoAsHidden(key: String, chainId: String, assetId: Int) {
toggleFeatureRepository.set(getKeyFor(key, chainId, assetId), true)
}
private fun getKeyFor(key: String, chainId: String, assetId: Int): String {
return "$key-$chainId-$assetId"
}
}
@@ -0,0 +1,12 @@
package io.novafoundation.nova.feature_ahm_impl.domain
import io.novafoundation.nova.feature_ahm_api.domain.model.ChainMigrationConfig
import io.novafoundation.nova.runtime.repository.ChainStateRepository
suspend fun ChainStateRepository.isMigrationBlockPassed(config: ChainMigrationConfig): Boolean {
return currentRemoteBlock(config.originData.chainId) > config.blockNumberStartAt
}
suspend fun ChainStateRepository.isMigrationBlockNotPassed(config: ChainMigrationConfig): Boolean {
return !isMigrationBlockPassed(config)
}
@@ -0,0 +1,8 @@
package io.novafoundation.nova.feature_ahm_impl.presentation
import io.novafoundation.nova.common.navigation.ReturnableRouter
interface ChainMigrationRouter : ReturnableRouter {
fun openChainMigrationDetails(chainId: String)
}
@@ -0,0 +1,53 @@
package io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.di.FeatureUtils
import io.novafoundation.nova.common.mixin.hints.setHints
import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents
import io.novafoundation.nova.common.utils.FragmentPayloadCreator
import io.novafoundation.nova.common.utils.PayloadCreator
import io.novafoundation.nova.common.utils.payload
import io.novafoundation.nova.feature_ahm_api.di.ChainMigrationFeatureApi
import io.novafoundation.nova.feature_ahm_impl.di.ChainMigrationFeatureComponent
import io.novafoundation.nova.feature_ahm_impl.databinding.FragmentChainMigrationDetailsBinding
import io.novafoundation.nova.feature_banners_api.presentation.bind
class ChainMigrationDetailsFragment : BaseFragment<ChainMigrationDetailsViewModel, FragmentChainMigrationDetailsBinding>() {
companion object : PayloadCreator<ChainMigrationDetailsPayload> by FragmentPayloadCreator()
override fun createBinding() = FragmentChainMigrationDetailsBinding.inflate(layoutInflater)
override fun initViews() {
binder.chainMigrationDetailsButton.setOnClickListener { viewModel.okButtonClicked() }
binder.chainMigrationDetailsToolbar.setRightActionClickListener { viewModel.learnMoreClicked() }
}
override fun inject() {
FeatureUtils.getFeature<ChainMigrationFeatureComponent>(
requireContext(),
ChainMigrationFeatureApi::class.java
).chainMigrationDetailsComponentFactory()
.create(this, payload())
.inject(this)
}
override fun subscribe(viewModel: ChainMigrationDetailsViewModel) {
observeBrowserEvents(viewModel)
viewModel.bannersFlow.observe {
it.bind(binder.chainMigrationDetailsBanner)
}
viewModel.configUIFlow.observe {
binder.chainMigrationDetailsTitle.text = it.title
binder.chainMigrationDetailsMinBalance.text = it.minimalBalance
binder.chainMigrationDetailsLowerFee.text = it.lowerFee
binder.chainMigrationDetailsTokens.text = it.tokens
binder.chainMigrationDetailsAccess.text = it.unifiedAccess
binder.chainMigrationDetailsAnyTokenFee.text = it.anyTokenFee
binder.chainMigrationDetailsHints.setHints(it.hints)
}
}
}
@@ -0,0 +1,7 @@
package io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class ChainMigrationDetailsPayload(val chainId: String) : Parcelable
@@ -0,0 +1,109 @@
package io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.mixin.api.Browserable
import io.novafoundation.nova.common.mixin.hints.HintModel
import io.novafoundation.nova.common.resources.ResourceManager
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.launchUnit
import io.novafoundation.nova.feature_ahm_api.domain.model.ChainMigrationConfig
import io.novafoundation.nova.feature_ahm_api.presentation.getChainMigrationDateFormat
import io.novafoundation.nova.feature_ahm_impl.R
import io.novafoundation.nova.feature_ahm_impl.domain.ChainMigrationDetailsInteractor
import io.novafoundation.nova.feature_ahm_impl.presentation.ChainMigrationRouter
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannersMixinFactory
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSourceFactory
import io.novafoundation.nova.feature_banners_api.presentation.source.forDirectory
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.TokenFormatter
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatToken
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class ChainMigrationDetailsViewModel(
private val resourceManager: ResourceManager,
private val router: ChainMigrationRouter,
private val interactor: ChainMigrationDetailsInteractor,
private val payload: ChainMigrationDetailsPayload,
private val promotionBannersMixinFactory: PromotionBannersMixinFactory,
private val bannerSourceFactory: BannersSourceFactory,
private val tokenFormatter: TokenFormatter
) : BaseViewModel(), Browserable {
override val openBrowserEvent = MutableLiveData<Event<String>>()
private val dateFormatter = getChainMigrationDateFormat()
private val chainFlow = interactor.chainFlow(payload.chainId)
.shareInBackground()
private val configFlow = flowOf { interactor.getChainMigrationConfig(payload.chainId) }
.filterNotNull()
.shareInBackground()
val bannersFlow = configFlow.map {
promotionBannersMixinFactory.create(bannerSourceFactory.forDirectory(it.bannerPath), this)
}.shareInBackground()
val configUIFlow = combine(configFlow, chainFlow) { config, sourceChain ->
val destinationChain = interactor.getChain(config.destinationData.chainId)
val sourceAsset = sourceChain.assetsById.getValue(config.originData.assetId)
val destinationAsset = destinationChain.assetsById.getValue(config.destinationData.assetId)
val tokenSymbol = sourceAsset.symbol.value
val newTokens = config.newTokenNames.joinToString()
val minimalBalanceScale = config.originData.minBalance / config.destinationData.minBalance
val lowerFeeScale = config.originData.averageFee / config.destinationData.averageFee
ConfigModel(
title = getTitle(config, tokenSymbol, destinationChain),
minimalBalance = resourceManager.getString(
R.string.chain_migration_details_minimal_balance,
minimalBalanceScale.format(),
tokenFormatter.formatToken(config.originData.minBalance, sourceAsset),
tokenFormatter.formatToken(config.destinationData.minBalance, destinationAsset),
),
lowerFee = resourceManager.getString(
R.string.chain_migration_details_lower_fee,
lowerFeeScale.format(),
tokenFormatter.formatToken(config.originData.averageFee, sourceAsset),
tokenFormatter.formatToken(config.destinationData.averageFee, destinationAsset)
),
tokens = resourceManager.getString(R.string.chain_migration_details_tokens, newTokens),
unifiedAccess = resourceManager.getString(R.string.chain_migration_details_unified_access, tokenSymbol),
anyTokenFee = resourceManager.getString(R.string.chain_migration_details_fee_in_any_tokens),
hints = listOf(
HintModel(R.drawable.ic_recent_history, resourceManager.getString(R.string.chain_migration_details_hint_history, sourceChain.name)),
HintModel(R.drawable.ic_pezkuwi, resourceManager.getString(R.string.chain_migration_details_hint_auto_migration))
)
)
}
private fun getTitle(config: ChainMigrationConfig, tokenSymbol: String, destinationChain: Chain): String {
val formattedDate = dateFormatter.format(config.timeStartAt)
return if (config.migrationInProgress) {
resourceManager.getString(R.string.chain_migration_details_in_progress_title, formattedDate, tokenSymbol, destinationChain.name)
} else {
resourceManager.getString(R.string.chain_migration_details_title, formattedDate, tokenSymbol, destinationChain.name)
}
}
fun okButtonClicked() = launchUnit {
interactor.markMigrationInfoAlreadyShown(payload.chainId)
router.back()
}
fun learnMoreClicked() {
launch {
val config = configFlow.first()
openBrowserEvent.value = Event(config.wikiURL)
}
}
}
@@ -0,0 +1,13 @@
package io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails
import io.novafoundation.nova.common.mixin.hints.HintModel
class ConfigModel(
val title: String,
val minimalBalance: String,
val lowerFee: String,
val tokens: String,
val unifiedAccess: String,
val anyTokenFee: String,
val hints: List<HintModel>
)
@@ -0,0 +1,34 @@
package io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails.deeplink
import android.net.Uri
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed
import io.novafoundation.nova.feature_ahm_impl.presentation.ChainMigrationRouter
import io.novafoundation.nova.feature_deep_linking.presentation.handling.CallbackEvent
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
import java.security.InvalidParameterException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
private const val STAKING_DASHBOARD_DEEP_LINK_PREFIX = "/open/ahm"
class ChainMigrationDetailsDeepLinkHandler(
private val chainMigrationRouter: ChainMigrationRouter,
private val automaticInteractionGate: AutomaticInteractionGate
) : DeepLinkHandler {
override val callbackFlow: Flow<CallbackEvent> = emptyFlow()
override suspend fun matches(data: Uri): Boolean {
val path = data.path ?: return false
return path.startsWith(STAKING_DASHBOARD_DEEP_LINK_PREFIX)
}
override suspend fun handleDeepLink(data: Uri) = runCatching {
automaticInteractionGate.awaitInteractionAllowed()
val chainId = data.getQueryParameter("chainId") ?: throw InvalidParameterException()
chainMigrationRouter.openChainMigrationDetails(chainId)
}
}
@@ -0,0 +1,28 @@
package io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails.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_ahm_impl.presentation.migrationDetails.ChainMigrationDetailsFragment
import io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails.ChainMigrationDetailsPayload
@Subcomponent(
modules = [
ChainMigrationDetailsModule::class
]
)
@ScreenScope
interface ChainMigrationDetailsComponent {
@Subcomponent.Factory
interface Factory {
fun create(
@BindsInstance fragment: Fragment,
@BindsInstance payload: ChainMigrationDetailsPayload
): ChainMigrationDetailsComponent
}
fun inject(fragment: ChainMigrationDetailsFragment)
}
@@ -0,0 +1,53 @@
package io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails.di
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
import io.novafoundation.nova.common.resources.ResourceManager
import io.novafoundation.nova.feature_ahm_impl.domain.ChainMigrationDetailsInteractor
import io.novafoundation.nova.feature_ahm_impl.presentation.ChainMigrationRouter
import io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails.ChainMigrationDetailsPayload
import io.novafoundation.nova.feature_ahm_impl.presentation.migrationDetails.ChainMigrationDetailsViewModel
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannersMixinFactory
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSourceFactory
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.TokenFormatter
@Module(includes = [ViewModelModule::class])
class ChainMigrationDetailsModule {
@Provides
@IntoMap
@ViewModelKey(ChainMigrationDetailsViewModel::class)
fun provideViewModel(
resourceManager: ResourceManager,
router: ChainMigrationRouter,
interactor: ChainMigrationDetailsInteractor,
payload: ChainMigrationDetailsPayload,
promotionBannersMixinFactory: PromotionBannersMixinFactory,
bannerSourceFactory: BannersSourceFactory,
tokenFormatter: TokenFormatter
): ViewModel {
return ChainMigrationDetailsViewModel(
resourceManager,
router,
interactor,
payload,
promotionBannersMixinFactory,
bannerSourceFactory,
tokenFormatter
)
}
@Provides
fun provideViewModelCreator(
fragment: Fragment,
viewModelFactory: ViewModelProvider.Factory
): ChainMigrationDetailsViewModel {
return ViewModelProvider(fragment, viewModelFactory).get(ChainMigrationDetailsViewModel::class.java)
}
}
@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<io.novafoundation.nova.common.view.Toolbar
android:id="@+id/chainMigrationDetailsToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:dividerVisible="false"
app:homeButtonVisible="false"
app:textRight="@string/common_learn_more" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="16dp">
<io.novafoundation.nova.feature_banners_api.presentation.view.BannerPagerView
android:id="@+id/chainMigrationDetailsBanner"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/chainMigrationDetailsTitle"
style="@style/TextAppearance.NovaFoundation.Bold.Title3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:textColor="@color/text_primary"
tools:text="Starting October 7, 2025 your KSM balance, Staking and Governance are now on Kusama Asset Hub" />
<TextView
android:id="@+id/chainMigrationDetailsSubtitle"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="4dp"
android:text="@string/chain_migration_details_subtitle"
android:textColor="@color/text_secondary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<TextView
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="👛" />
<TextView
android:id="@+id/chainMigrationDetailsMinBalance"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="top"
tools:text="100x reduction in minimal balance (from 0.00033 KSM to 0.0000033 KSM)" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="💸" />
<TextView
android:id="@+id/chain_migration_details_lower_fee"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="top"
tools:text="10x lower transaction fees(from 0.0005 KSM to 0.00005 KSM)" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="🪙" />
<TextView
android:id="@+id/chainMigrationDetailsTokens"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="top"
tools:text="More tokens supported: USDT, USDC, ETH, DOT, and other ecosystem tokens" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="🗂️" />
<TextView
android:id="@+id/chainMigrationDetailsAccess"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="top"
tools:text="Unified access to KSM, assets, staking, and governance" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="🧾" />
<TextView
android:id="@+id/chainMigrationDetailsAnyTokenFee"
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="top"
tools:text="Ability to pay fees in any token" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:background="@color/divider" />
<io.novafoundation.nova.common.mixin.hints.HintsView
android:id="@+id/chainMigrationDetailsHints"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" />
</LinearLayout>
</ScrollView>
<io.novafoundation.nova.common.view.PrimaryButtonV2
android:id="@+id/chainMigrationDetailsButton"
style="@style/Widget.Nova.MaterialButton.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="16dp"
android:text="@string/common_got_it" />
</LinearLayout>
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>