mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 19:37:55 +00:00
Initial commit: Pezkuwi Wallet Android
Complete rebrand of Nova Wallet for Pezkuwichain ecosystem. ## Features - Full Pezkuwichain support (HEZ & PEZ tokens) - Polkadot ecosystem compatibility - Staking, Governance, DeFi, NFTs - XCM cross-chain transfers - Hardware wallet support (Ledger, Polkadot Vault) - WalletConnect v2 - Push notifications ## Languages - English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese Based on Nova Wallet by Novasama Technologies GmbH © Dijital Kurdistan Tech Institute 2026
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.data
|
||||
|
||||
import io.novafoundation.nova.common.data.storage.encrypt.EncryptedPreferences
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val GIFT_SECRETS = "GIFT_SECRETS"
|
||||
|
||||
interface GiftSecretsRepository {
|
||||
|
||||
suspend fun putGiftAccountSeed(accountId: ByteArray, seed: ByteArray)
|
||||
|
||||
suspend fun getGiftAccountSeed(accountId: ByteArray): ByteArray?
|
||||
}
|
||||
|
||||
class RealGiftSecretsRepository(
|
||||
private val encryptedPreferences: EncryptedPreferences,
|
||||
) : GiftSecretsRepository {
|
||||
|
||||
override suspend fun putGiftAccountSeed(accountId: ByteArray, seed: ByteArray) = withContext(Dispatchers.IO) {
|
||||
encryptedPreferences.putEncryptedString(giftAccountKey(accountId), seed.toHexString())
|
||||
}
|
||||
|
||||
override suspend fun getGiftAccountSeed(accountId: ByteArray): ByteArray? = withContext(Dispatchers.IO) {
|
||||
encryptedPreferences.getDecryptedString(giftAccountKey(accountId))?.fromHex()
|
||||
}
|
||||
|
||||
private fun giftAccountKey(accountId: ByteArray) = "${accountId.toHexString()}:$GIFT_SECRETS"
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.data
|
||||
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.core_db.dao.GiftsDao
|
||||
import io.novafoundation.nova.core_db.model.GiftLocal
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.Gift
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import java.math.BigInteger
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
interface GiftsRepository {
|
||||
|
||||
suspend fun getGift(giftId: Long): Gift
|
||||
|
||||
suspend fun getGifts(): List<Gift>
|
||||
|
||||
fun observeGift(giftId: Long): Flow<Gift>
|
||||
|
||||
fun observeGifts(): Flow<List<Gift>>
|
||||
|
||||
suspend fun saveNewGift(
|
||||
accountIdKey: AccountIdKey,
|
||||
amount: BigInteger,
|
||||
creatorMetaId: Long,
|
||||
fullChainAssetId: FullChainAssetId
|
||||
): Long
|
||||
|
||||
suspend fun setGiftState(id: Long, status: Gift.Status)
|
||||
}
|
||||
|
||||
class RealGiftsRepository(
|
||||
private val giftsDao: GiftsDao
|
||||
) : GiftsRepository {
|
||||
|
||||
override suspend fun getGift(giftId: Long): Gift {
|
||||
return giftsDao.getGiftById(giftId).toDomain()
|
||||
}
|
||||
|
||||
override suspend fun getGifts(): List<Gift> {
|
||||
return giftsDao.getAllGifts().map { it.toDomain() }
|
||||
}
|
||||
|
||||
override fun observeGift(giftId: Long): Flow<Gift> {
|
||||
return giftsDao.observeGiftById(giftId)
|
||||
.map { it.toDomain() }
|
||||
}
|
||||
|
||||
override fun observeGifts(): Flow<List<Gift>> {
|
||||
return giftsDao.observeAllGifts()
|
||||
.mapList { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun saveNewGift(
|
||||
accountIdKey: AccountIdKey,
|
||||
amount: BigInteger,
|
||||
creatorMetaId: Long,
|
||||
fullChainAssetId: FullChainAssetId
|
||||
): Long {
|
||||
return giftsDao.createNewGift(
|
||||
GiftLocal(
|
||||
amount = amount,
|
||||
giftAccountId = accountIdKey.value,
|
||||
creatorMetaId = creatorMetaId,
|
||||
chainId = fullChainAssetId.chainId,
|
||||
assetId = fullChainAssetId.assetId,
|
||||
status = GiftLocal.Status.PENDING,
|
||||
creationDate = Date().time
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setGiftState(id: Long, status: Gift.Status) {
|
||||
giftsDao.setGiftState(id, status.toDomain())
|
||||
}
|
||||
|
||||
private fun GiftLocal.toDomain() = Gift(
|
||||
id = id,
|
||||
creatorMetaId = creatorMetaId,
|
||||
chainId = chainId,
|
||||
giftAccountId = giftAccountId,
|
||||
assetId = assetId,
|
||||
amount = amount,
|
||||
status = when (status) {
|
||||
GiftLocal.Status.PENDING -> Gift.Status.PENDING
|
||||
GiftLocal.Status.CLAIMED -> Gift.Status.CLAIMED
|
||||
GiftLocal.Status.RECLAIMED -> Gift.Status.RECLAIMED
|
||||
},
|
||||
creationDate = Date(this.creationDate)
|
||||
)
|
||||
|
||||
private fun Gift.Status.toDomain(): GiftLocal.Status {
|
||||
return when (this) {
|
||||
Gift.Status.PENDING -> GiftLocal.Status.PENDING
|
||||
Gift.Status.CLAIMED -> GiftLocal.Status.CLAIMED
|
||||
Gift.Status.RECLAIMED -> GiftLocal.Status.RECLAIMED
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package io.novafoundation.nova.feature_gift_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_deep_linking.di.DeepLinkingFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_impl.di.modules.deeplinks.DeepLinkModule
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.amount.di.SelectGiftAmountComponent
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.claim.di.ClaimGiftComponent
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.confirm.di.CreateGiftConfirmComponent
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.gifts.di.GiftsComponent
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.share.di.ShareGiftComponent
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
GiftFeatureDependencies::class
|
||||
],
|
||||
modules = [
|
||||
GiftFeatureModule::class,
|
||||
DeepLinkModule::class
|
||||
]
|
||||
)
|
||||
@FeatureScope
|
||||
interface GiftFeatureComponent : GiftFeatureApi {
|
||||
|
||||
fun giftsComponentFactory(): GiftsComponent.Factory
|
||||
|
||||
fun selectGiftAmountComponentFactory(): SelectGiftAmountComponent.Factory
|
||||
|
||||
fun createGiftConfirmComponentFactory(): CreateGiftConfirmComponent.Factory
|
||||
|
||||
fun shareGiftComponentFactory(): ShareGiftComponent.Factory
|
||||
|
||||
fun claimGiftComponentFactory(): ClaimGiftComponent.Factory
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance router: GiftRouter,
|
||||
deps: GiftFeatureDependencies
|
||||
): GiftFeatureComponent
|
||||
}
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
CommonApi::class,
|
||||
AccountFeatureApi::class,
|
||||
DbApi::class,
|
||||
WalletFeatureApi::class,
|
||||
RuntimeApi::class,
|
||||
DeepLinkingFeatureApi::class
|
||||
]
|
||||
)
|
||||
interface GiftFeatureDependenciesComponent : GiftFeatureDependencies
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.di
|
||||
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.data.memory.ComputationalCache
|
||||
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.data.storage.Preferences
|
||||
import io.novafoundation.nova.common.interfaces.FileProvider
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.data.storage.encrypt.EncryptedPreferences
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.DialogMessageManager
|
||||
import io.novafoundation.nova.common.utils.IntegrityService
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.core_db.dao.GiftsDao
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade
|
||||
import io.novafoundation.nova.feature_account_api.data.repository.CreateSecretsRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults
|
||||
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.CreateGiftMetaAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectSingleWallet.SelectSingleWalletMixin
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.navigation.ExtrinsicNavigationWrapper
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilderFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.common.fieldValidator.EnoughAmountValidatorFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.common.fieldValidator.MinAmountFieldValidatorFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.TokenFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.getAsset.GetAssetOptionsMixin
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.maxAction.MaxActionProviderFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
interface GiftFeatureDependencies {
|
||||
|
||||
val amountFormatter: AmountFormatter
|
||||
|
||||
val context: Context
|
||||
|
||||
val preferences: Preferences
|
||||
|
||||
val integrityService: IntegrityService
|
||||
|
||||
val getAssetOptionsMixinFactory: GetAssetOptionsMixin.Factory
|
||||
|
||||
val assetSourceRegistry: AssetSourceRegistry
|
||||
|
||||
val validationExecutor: ValidationExecutor
|
||||
|
||||
val maxActionProviderFactory: MaxActionProviderFactory
|
||||
|
||||
val selectedAccountUseCase: SelectedAccountUseCase
|
||||
|
||||
val enoughAmountValidatorFactory: EnoughAmountValidatorFactory
|
||||
|
||||
val minAmountFieldValidatorFactory: MinAmountFieldValidatorFactory
|
||||
|
||||
val amountChooserMixinFactory: AmountChooserMixin.Factory
|
||||
|
||||
val feeLoaderMixinV2Factory: FeeLoaderMixinV2.Factory
|
||||
|
||||
val assetUseCase: ArbitraryAssetUseCase
|
||||
|
||||
val createSecretsRepository: CreateSecretsRepository
|
||||
|
||||
val sendUseCase: SendUseCase
|
||||
|
||||
val walletUiUseCase: WalletUiUseCase
|
||||
|
||||
val externalAccountActions: ExternalActions.Presentation
|
||||
|
||||
val addressIconGenerator: AddressIconGenerator
|
||||
|
||||
val extrinsicNavigationWrapper: ExtrinsicNavigationWrapper
|
||||
|
||||
val encryptionDefaults: EncryptionDefaults
|
||||
|
||||
val encryptedPreferences: EncryptedPreferences
|
||||
|
||||
val linkBuilderFactory: LinkBuilderFactory
|
||||
|
||||
val assetIconProvider: AssetIconProvider
|
||||
|
||||
val tokenFormatter: TokenFormatter
|
||||
|
||||
val fileProvider: FileProvider
|
||||
|
||||
val createGiftMetaAccountUseCase: CreateGiftMetaAccountUseCase
|
||||
|
||||
val automaticInteractionGate: AutomaticInteractionGate
|
||||
|
||||
val dialogMessageManager: DialogMessageManager
|
||||
|
||||
val accountRepository: AccountRepository
|
||||
|
||||
val selectSingleWalletMixin: SelectSingleWalletMixin.Factory
|
||||
|
||||
val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
|
||||
|
||||
val accountInteractor: AccountInteractor
|
||||
|
||||
val computationalCache: ComputationalCache
|
||||
|
||||
val feePaymentRegistry: FeePaymentProviderRegistry
|
||||
|
||||
val feePaymentFacade: CustomFeeCapabilityFacade
|
||||
|
||||
fun giftsDao(): GiftsDao
|
||||
|
||||
fun resourceManager(): ResourceManager
|
||||
|
||||
fun appLinksProvider(): AppLinksProvider
|
||||
|
||||
fun chainRegistry(): ChainRegistry
|
||||
|
||||
fun imageLoader(): ImageLoader
|
||||
|
||||
fun secretStoreV2(): SecretStoreV2
|
||||
|
||||
fun apiCreator(): NetworkApiCreator
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_gift_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_deep_linking.di.DeepLinkingFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
import javax.inject.Inject
|
||||
|
||||
@ApplicationScope
|
||||
class GiftFeatureHolder @Inject constructor(
|
||||
featureContainer: FeatureContainer,
|
||||
private val router: GiftRouter
|
||||
) : FeatureApiHolder(featureContainer) {
|
||||
|
||||
override fun initializeDependencies(): Any {
|
||||
val dependencies = DaggerGiftFeatureComponent_GiftFeatureDependenciesComponent.builder()
|
||||
.commonApi(commonApi())
|
||||
.dbApi(getFeature(DbApi::class.java))
|
||||
.walletFeatureApi(getFeature(WalletFeatureApi::class.java))
|
||||
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
|
||||
.runtimeApi(getFeature(RuntimeApi::class.java))
|
||||
.deepLinkingFeatureApi(getFeature(DeepLinkingFeatureApi::class.java))
|
||||
.build()
|
||||
|
||||
return DaggerGiftFeatureComponent.factory()
|
||||
.create(router, dependencies)
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.memory.ComputationalCache
|
||||
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
|
||||
import io.novafoundation.nova.common.data.storage.encrypt.EncryptedPreferences
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.dao.GiftsDao
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade
|
||||
import io.novafoundation.nova.feature_account_api.data.repository.CreateSecretsRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.CreateGiftMetaAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_gift_api.domain.GiftsAccountSupportedUseCase
|
||||
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.feature_gift_impl.data.GiftSecretsRepository
|
||||
import io.novafoundation.nova.feature_gift_impl.data.GiftsRepository
|
||||
import io.novafoundation.nova.feature_gift_impl.data.RealGiftSecretsRepository
|
||||
import io.novafoundation.nova.feature_gift_impl.data.RealGiftsRepository
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.ClaimGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.GiftsInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.RealGiftsInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.RealCreateGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.CreateGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.GiftSecretsUseCase
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.RealGiftsAccountSupportedUseCase
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.RealAvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.RealClaimGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.RealShareGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.ShareGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.amount.GiftMinAmountProviderFactory
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.PackingGiftAnimationFactory
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.UnpackingGiftAnimationFactory
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.claim.ClaimGiftMixinFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module()
|
||||
class GiftFeatureModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providesGiftsRepository(
|
||||
giftsDao: GiftsDao
|
||||
): GiftsRepository {
|
||||
return RealGiftsRepository(giftsDao)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providesGiftsInteractor(
|
||||
repository: GiftsRepository,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
chainRegistry: ChainRegistry,
|
||||
selectedAccountUseCase: SelectedAccountUseCase
|
||||
): GiftsInteractor {
|
||||
return RealGiftsInteractor(repository, assetSourceRegistry, chainRegistry, selectedAccountUseCase)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providesGiftSecretsRepository(encryptedPreferences: EncryptedPreferences): GiftSecretsRepository {
|
||||
return RealGiftSecretsRepository(encryptedPreferences)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideSelectGiftAmountInteractor(
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
chainRegistry: ChainRegistry,
|
||||
giftSecretsRepository: GiftSecretsRepository,
|
||||
giftsRepository: GiftsRepository,
|
||||
sendUseCase: SendUseCase,
|
||||
giftSecretsUseCase: GiftSecretsUseCase
|
||||
): CreateGiftInteractor {
|
||||
return RealCreateGiftInteractor(
|
||||
assetSourceRegistry,
|
||||
chainRegistry,
|
||||
giftSecretsRepository,
|
||||
giftsRepository,
|
||||
sendUseCase,
|
||||
giftSecretsUseCase
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideGiftMinAmountProviderFactory(
|
||||
createGiftInteractor: CreateGiftInteractor
|
||||
): GiftMinAmountProviderFactory {
|
||||
return GiftMinAmountProviderFactory(createGiftInteractor)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providesShareGiftInteractor(
|
||||
giftsRepository: GiftsRepository,
|
||||
giftSecretsRepository: GiftSecretsRepository
|
||||
): ShareGiftInteractor {
|
||||
return RealShareGiftInteractor(giftsRepository, giftSecretsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providesPackingGiftAnimationFactory(): PackingGiftAnimationFactory {
|
||||
return PackingGiftAnimationFactory()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providesUnpackingGiftAnimationFactory(): UnpackingGiftAnimationFactory {
|
||||
return UnpackingGiftAnimationFactory()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideClaimGiftInteractor(
|
||||
giftSecretsUseCase: GiftSecretsUseCase,
|
||||
chainRegistry: ChainRegistry,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
sendUseCase: SendUseCase,
|
||||
createGiftMetaAccountUseCase: CreateGiftMetaAccountUseCase,
|
||||
secretStoreV2: SecretStoreV2,
|
||||
accountRepository: AccountRepository
|
||||
): ClaimGiftInteractor {
|
||||
return RealClaimGiftInteractor(
|
||||
giftSecretsUseCase,
|
||||
chainRegistry,
|
||||
assetSourceRegistry,
|
||||
sendUseCase,
|
||||
createGiftMetaAccountUseCase,
|
||||
secretStoreV2,
|
||||
accountRepository
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideGiftSecretsUseCase(
|
||||
createSecretsRepository: CreateSecretsRepository,
|
||||
encryptionDefaults: EncryptionDefaults
|
||||
): GiftSecretsUseCase {
|
||||
return GiftSecretsUseCase(
|
||||
createSecretsRepository,
|
||||
encryptionDefaults
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideClaimGiftMixinFactory(claimGiftInteractor: ClaimGiftInteractor) = ClaimGiftMixinFactory(claimGiftInteractor)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAvailableGiftAssetsUseCase(
|
||||
chainRegistry: ChainRegistry,
|
||||
computationalCache: ComputationalCache,
|
||||
feePaymentRegistry: FeePaymentProviderRegistry,
|
||||
feePaymentFacade: CustomFeeCapabilityFacade,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
): AvailableGiftAssetsUseCase {
|
||||
return RealAvailableGiftAssetsUseCase(
|
||||
chainRegistry,
|
||||
computationalCache,
|
||||
feePaymentRegistry,
|
||||
feePaymentFacade,
|
||||
assetSourceRegistry
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideAreGiftsSupportedUseCase(selectedAccountUseCase: SelectedAccountUseCase): GiftsAccountSupportedUseCase {
|
||||
return RealGiftsAccountSupportedUseCase(selectedAccountUseCase)
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.di.modules.deeplinks
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.DialogMessageManager
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilderFactory
|
||||
import io.novafoundation.nova.feature_gift_api.di.GiftDeepLinks
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.ClaimGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.claim.deeplink.ClaimGiftDeepLinkConfigurator
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.claim.deeplink.ClaimGiftDeepLinkHandler
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module
|
||||
class DeepLinkModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideDeepLinkConfigurator(
|
||||
linkBuilderFactory: LinkBuilderFactory
|
||||
): ClaimGiftDeepLinkConfigurator {
|
||||
return ClaimGiftDeepLinkConfigurator(linkBuilderFactory)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideClaimGiftDeepLinkHandler(
|
||||
router: GiftRouter,
|
||||
chainRegistry: ChainRegistry,
|
||||
automaticInteractionGate: AutomaticInteractionGate,
|
||||
dialogMessageManager: DialogMessageManager,
|
||||
claimGiftDeepLinkConfigurator: ClaimGiftDeepLinkConfigurator,
|
||||
claimGiftInteractor: ClaimGiftInteractor
|
||||
): ClaimGiftDeepLinkHandler {
|
||||
return ClaimGiftDeepLinkHandler(
|
||||
router,
|
||||
chainRegistry,
|
||||
automaticInteractionGate,
|
||||
dialogMessageManager,
|
||||
claimGiftDeepLinkConfigurator,
|
||||
claimGiftInteractor
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideDeepLinks(
|
||||
claimGiftDeepLinkInteractor: ClaimGiftDeepLinkHandler
|
||||
): GiftDeepLinks {
|
||||
return GiftDeepLinks(listOf(claimGiftDeepLinkInteractor))
|
||||
}
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain
|
||||
|
||||
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
|
||||
import io.novafoundation.nova.common.data.secrets.v2.keypair
|
||||
import io.novafoundation.nova.common.data.secrets.v2.publicKey
|
||||
import io.novafoundation.nova.common.utils.coerceToUnit
|
||||
import io.novafoundation.nova.common.utils.finally
|
||||
import io.novafoundation.nova.common.utils.isZero
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency.Asset.Companion.toFeePaymentCurrency
|
||||
import io.novafoundation.nova.feature_account_api.data.model.decimalAmount
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.CreateGiftMetaAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.isControllableWallet
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.ClaimableGift
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.GiftAmountWithFee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.BaseAssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.asWeighted
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOf
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.ext.emptyAccountId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
interface ClaimGiftInteractor {
|
||||
|
||||
suspend fun getClaimableGift(secret: ByteArray, chainId: String, assetId: Int): ClaimableGift
|
||||
|
||||
suspend fun getGiftAmountWithFee(
|
||||
claimableGift: ClaimableGift,
|
||||
giftMetaAccount: MetaAccount,
|
||||
coroutineScope: CoroutineScope
|
||||
): GiftAmountWithFee
|
||||
|
||||
suspend fun createTempMetaAccount(claimableGift: ClaimableGift): MetaAccount
|
||||
|
||||
suspend fun isGiftAlreadyClaimed(claimableGift: ClaimableGift): Boolean
|
||||
|
||||
suspend fun claimGift(
|
||||
claimableGift: ClaimableGift,
|
||||
giftAmountWithFee: GiftAmountWithFee,
|
||||
giftMetaAccount: MetaAccount,
|
||||
giftRecipient: MetaAccount,
|
||||
coroutineScope: CoroutineScope
|
||||
): Result<Unit>
|
||||
|
||||
suspend fun getMetaAccount(metaId: Long): MetaAccount
|
||||
|
||||
suspend fun getMetaAccountToClaimGift(): MetaAccount
|
||||
}
|
||||
|
||||
class RealClaimGiftInteractor(
|
||||
private val giftSecretsUseCase: GiftSecretsUseCase,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val sendUseCase: SendUseCase,
|
||||
private val createGiftMetaAccountUseCase: CreateGiftMetaAccountUseCase,
|
||||
private val secretStoreV2: SecretStoreV2,
|
||||
private val accountRepository: AccountRepository
|
||||
) : ClaimGiftInteractor {
|
||||
|
||||
override suspend fun getClaimableGift(secret: ByteArray, chainId: String, assetId: Int): ClaimableGift {
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
val giftSecrets = giftSecretsUseCase.createGiftSecrets(chain, secret)
|
||||
return ClaimableGift(
|
||||
accountId = chain.accountIdOf(giftSecrets.keypair.publicKey),
|
||||
chain = chain,
|
||||
chainAsset = chain.assetsById.getValue(assetId),
|
||||
secrets = giftSecrets
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun createTempMetaAccount(claimableGift: ClaimableGift): MetaAccount {
|
||||
return createGiftMetaAccountUseCase.createTemporaryGiftMetaAccount(claimableGift.chain, claimableGift.secrets)
|
||||
}
|
||||
|
||||
override suspend fun isGiftAlreadyClaimed(claimableGift: ClaimableGift): Boolean {
|
||||
val giftBalance = getGiftAccountBalance(claimableGift)
|
||||
return giftBalance.isZero
|
||||
}
|
||||
|
||||
override suspend fun getGiftAmountWithFee(
|
||||
claimableGift: ClaimableGift,
|
||||
giftMetaAccount: MetaAccount,
|
||||
coroutineScope: CoroutineScope
|
||||
): GiftAmountWithFee {
|
||||
val accountBalance = claimableGift.chainAsset.amountFromPlanks(getGiftAccountBalance(claimableGift))
|
||||
val transferModel = createTransfer(
|
||||
claimableGift,
|
||||
giftMetaAccount,
|
||||
recipientAccountId = claimableGift.chain.emptyAccountId(),
|
||||
accountBalance
|
||||
)
|
||||
val claimFee = assetSourceRegistry.sourceFor(claimableGift.chainAsset)
|
||||
.transfers
|
||||
.calculateFee(transferModel, coroutineScope)
|
||||
|
||||
return GiftAmountWithFee(accountBalance - claimFee.decimalAmount, claimFee)
|
||||
}
|
||||
|
||||
override suspend fun claimGift(
|
||||
claimableGift: ClaimableGift,
|
||||
giftAmountWithFee: GiftAmountWithFee,
|
||||
giftMetaAccount: MetaAccount,
|
||||
giftRecipient: MetaAccount,
|
||||
coroutineScope: CoroutineScope
|
||||
): Result<Unit> {
|
||||
// Put secrets for temporary meta account in storage but for this operation only since signer logic requires secrets in secret storage
|
||||
secretStoreV2.putChainAccountSecrets(giftMetaAccount.id, claimableGift.accountId, claimableGift.secrets)
|
||||
|
||||
return claimGiftInternal(
|
||||
giftModel = claimableGift,
|
||||
giftMetaAccount = giftMetaAccount,
|
||||
giftRecipient = giftRecipient,
|
||||
giftAmountWithFee = giftAmountWithFee,
|
||||
coroutineScope = coroutineScope
|
||||
)
|
||||
.finally {
|
||||
// Remove secrets for temporary meta account from storage after claim or failure
|
||||
secretStoreV2.clearChainAccountsSecrets(giftMetaAccount.id, listOf(claimableGift.accountId))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMetaAccount(metaId: Long): MetaAccount {
|
||||
return accountRepository.getMetaAccount(metaId)
|
||||
}
|
||||
|
||||
override suspend fun getMetaAccountToClaimGift(): MetaAccount {
|
||||
val selectedMetaAccount = accountRepository.getSelectedMetaAccount()
|
||||
if (selectedMetaAccount.type.isControllableWallet()) return selectedMetaAccount
|
||||
|
||||
val firstControllableWallet = accountRepository.getActiveMetaAccounts()
|
||||
.firstOrNull { it.type.isControllableWallet() }
|
||||
|
||||
return firstControllableWallet ?: selectedMetaAccount
|
||||
}
|
||||
|
||||
private suspend fun claimGiftInternal(
|
||||
giftModel: ClaimableGift,
|
||||
giftMetaAccount: MetaAccount,
|
||||
giftRecipient: MetaAccount,
|
||||
giftAmountWithFee: GiftAmountWithFee,
|
||||
coroutineScope: CoroutineScope
|
||||
): Result<Unit> {
|
||||
val originFee = OriginFee(submissionFee = giftAmountWithFee.fee, deliveryFee = null)
|
||||
val giftTransfer = createTransfer(
|
||||
giftModel,
|
||||
giftMetaAccount,
|
||||
recipientAccountId = giftRecipient.requireAccountIdIn(giftModel.chain),
|
||||
amount = giftAmountWithFee.amount
|
||||
).asWeighted(originFee)
|
||||
|
||||
return sendUseCase.performOnChainTransferAndAwaitExecution(giftTransfer, originFee.submissionFee, coroutineScope)
|
||||
.coerceToUnit()
|
||||
}
|
||||
|
||||
private suspend fun getGiftAccountBalance(claimableGift: ClaimableGift): BigInteger {
|
||||
val assetBalanceSource = assetSourceRegistry.sourceFor(claimableGift.chainAsset).balance
|
||||
|
||||
return assetBalanceSource.queryAccountBalance(
|
||||
claimableGift.chain,
|
||||
claimableGift.chainAsset,
|
||||
claimableGift.accountId
|
||||
).total
|
||||
}
|
||||
|
||||
private fun createTransfer(
|
||||
giftModel: ClaimableGift,
|
||||
giftMetaAccount: MetaAccount,
|
||||
recipientAccountId: AccountId,
|
||||
amount: BigDecimal
|
||||
): BaseAssetTransfer {
|
||||
return BaseAssetTransfer(
|
||||
sender = giftMetaAccount,
|
||||
recipient = giftModel.chain.addressOf(recipientAccountId),
|
||||
originChain = giftModel.chain,
|
||||
originChainAsset = giftModel.chainAsset,
|
||||
destinationChain = giftModel.chain,
|
||||
destinationChainAsset = giftModel.chainAsset,
|
||||
feePaymentCurrency = giftModel.chainAsset.toFeePaymentCurrency(),
|
||||
amount = amount,
|
||||
transferringMaxAmount = true
|
||||
)
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.address.AccountIdKey
|
||||
import io.novafoundation.nova.common.address.intoKey
|
||||
import io.novafoundation.nova.common.data.secrets.v2.keypair
|
||||
import io.novafoundation.nova.common.data.secrets.v2.publicKey
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency.Asset.Companion.toFeePaymentCurrency
|
||||
import io.novafoundation.nova.feature_account_api.data.model.EvmFee
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
|
||||
import io.novafoundation.nova.feature_gift_impl.data.GiftSecretsRepository
|
||||
import io.novafoundation.nova.feature_gift_impl.data.GiftsRepository
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.CreateGiftModel
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.GiftFee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.existentialDeposit
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfersValidationSystem
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.BaseAssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.SendUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.planksFromAmount
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOf
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.ext.emptyAccountIdKey
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
||||
typealias GiftId = Long
|
||||
|
||||
interface CreateGiftInteractor {
|
||||
fun validationSystemFor(chainAsset: Chain.Asset, coroutineScope: CoroutineScope): AssetTransfersValidationSystem
|
||||
|
||||
suspend fun getFee(
|
||||
model: CreateGiftModel,
|
||||
transferAllToCreateGift: Boolean,
|
||||
coroutineScope: CoroutineScope
|
||||
): GiftFee
|
||||
|
||||
suspend fun getExistentialDeposit(chainAsset: Chain.Asset): BigDecimal
|
||||
|
||||
suspend fun createAndSaveGift(
|
||||
giftModel: CreateGiftModel,
|
||||
transfer: WeightedAssetTransfer,
|
||||
fee: SubmissionFee,
|
||||
coroutineScope: CoroutineScope
|
||||
): Result<GiftId>
|
||||
}
|
||||
|
||||
class RealCreateGiftInteractor(
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val giftSecretsRepository: GiftSecretsRepository,
|
||||
private val giftsRepository: GiftsRepository,
|
||||
private val sendUseCase: SendUseCase,
|
||||
private val giftSecretsUseCase: GiftSecretsUseCase
|
||||
) : CreateGiftInteractor {
|
||||
|
||||
override fun validationSystemFor(chainAsset: Chain.Asset, coroutineScope: CoroutineScope): AssetTransfersValidationSystem {
|
||||
return getAssetTransfers(chainAsset)
|
||||
.getValidationSystem(coroutineScope)
|
||||
}
|
||||
|
||||
override suspend fun getFee(
|
||||
model: CreateGiftModel,
|
||||
transferAllToCreateGift: Boolean,
|
||||
coroutineScope: CoroutineScope
|
||||
): GiftFee = withContext(Dispatchers.Default) {
|
||||
val claimGiftFee = getSubmissionFee(
|
||||
model = model,
|
||||
transferMax = true,
|
||||
giftAccountId = model.chain.emptyAccountIdKey(),
|
||||
coroutineScope = coroutineScope
|
||||
).doubleFeeForEvm()
|
||||
val claimFeeAmount = model.chainAsset.amountFromPlanks(claimGiftFee.amount)
|
||||
|
||||
val createGiftFee = getSubmissionFee(
|
||||
model = model.copy(amount = model.amount + claimFeeAmount),
|
||||
transferMax = transferAllToCreateGift,
|
||||
giftAccountId = model.chain.emptyAccountIdKey(),
|
||||
coroutineScope = coroutineScope
|
||||
)
|
||||
GiftFee(
|
||||
createGiftFee = createGiftFee,
|
||||
claimGiftFee = claimGiftFee
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getExistentialDeposit(chainAsset: Chain.Asset): BigDecimal {
|
||||
return assetSourceRegistry.existentialDeposit(chainAsset)
|
||||
}
|
||||
|
||||
override suspend fun createAndSaveGift(
|
||||
giftModel: CreateGiftModel,
|
||||
transfer: WeightedAssetTransfer,
|
||||
fee: SubmissionFee,
|
||||
coroutineScope: CoroutineScope
|
||||
): Result<GiftId> {
|
||||
val giftAccountId = createAndStoreRandomGiftAccount(giftModel.chain.id)
|
||||
val gitAddress = giftModel.chain.addressOf(giftAccountId)
|
||||
val giftTransfer = transfer.copy(recipient = gitAddress)
|
||||
return sendUseCase.performOnChainTransferAndAwaitExecution(giftTransfer, fee, coroutineScope)
|
||||
.map {
|
||||
Log.d(LOG_TAG, "Gift was created successfully. Address in ${giftModel.chain.name}: $gitAddress")
|
||||
|
||||
giftsRepository.saveNewGift(
|
||||
accountIdKey = giftAccountId,
|
||||
amount = giftModel.chainAsset.planksFromAmount(giftModel.amount),
|
||||
creatorMetaId = giftModel.senderMetaAccount.id,
|
||||
fullChainAssetId = giftModel.chainAsset.fullId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSubmissionFee(
|
||||
model: CreateGiftModel,
|
||||
transferMax: Boolean,
|
||||
giftAccountId: AccountIdKey,
|
||||
coroutineScope: CoroutineScope
|
||||
): SubmissionFee {
|
||||
return withContext(Dispatchers.Default) {
|
||||
val transfer = model.mapToAssetTransfer(giftAccountId, transferMax)
|
||||
getAssetTransfers(model.chainAsset).calculateFee(transfer, coroutineScope = coroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAssetTransfers(chainAsset: Chain.Asset) = assetSourceRegistry.sourceFor(chainAsset).transfers
|
||||
|
||||
private fun CreateGiftModel.mapToAssetTransfer(giftAccountId: AccountIdKey, transferMax: Boolean) = BaseAssetTransfer(
|
||||
sender = senderMetaAccount,
|
||||
recipient = chain.addressOf(giftAccountId),
|
||||
originChain = chain,
|
||||
originChainAsset = chainAsset,
|
||||
destinationChain = chain,
|
||||
destinationChainAsset = chainAsset,
|
||||
feePaymentCurrency = chainAsset.toFeePaymentCurrency(),
|
||||
amount = amount,
|
||||
transferringMaxAmount = transferMax
|
||||
)
|
||||
|
||||
private suspend fun createAndStoreRandomGiftAccount(chainId: String): AccountIdKey {
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
val giftSeed = giftSecretsUseCase.createRandomGiftSeed()
|
||||
val giftSecrets = giftSecretsUseCase.createGiftSecrets(chain, giftSeed)
|
||||
|
||||
val accountId = chain.accountIdOf(giftSecrets.keypair.publicKey)
|
||||
giftSecretsRepository.putGiftAccountSeed(accountId, giftSeed)
|
||||
|
||||
return accountId.intoKey()
|
||||
}
|
||||
|
||||
private fun SubmissionFee.doubleFeeForEvm(): SubmissionFee {
|
||||
return when (this) {
|
||||
is EvmFee -> copy(gasLimit = gasLimit * 2.toBigInteger())
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain
|
||||
|
||||
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
|
||||
import io.novafoundation.nova.feature_account_api.data.repository.CreateSecretsRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.common.EncryptionDefaults
|
||||
import io.novafoundation.nova.feature_account_api.domain.account.common.forChain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.encrypt.seed.SeedCreator
|
||||
import io.novasama.substrate_sdk_android.scale.EncodableStruct
|
||||
import org.bouncycastle.crypto.generators.SCrypt
|
||||
|
||||
private const val GIFT_SEED_SIZE_BYTES = 10
|
||||
|
||||
class GiftSecretsUseCase(
|
||||
private val createSecretsRepository: CreateSecretsRepository,
|
||||
private val encryptionDefaults: EncryptionDefaults
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val GIFT_SALT = "gift"
|
||||
private const val SCRYPT_KEY_SIZE = 32
|
||||
private const val N = 16384
|
||||
private const val p = 1
|
||||
private const val r = 8
|
||||
}
|
||||
|
||||
suspend fun createGiftSecrets(chain: Chain, seed: ByteArray): EncodableStruct<ChainAccountSecrets> {
|
||||
val encryption = encryptionDefaults.forChain(chain)
|
||||
|
||||
return createSecretsRepository.createSecretsWithSeed(
|
||||
seed = getGiftSeedHash(seed),
|
||||
cryptoType = encryption.cryptoType,
|
||||
derivationPath = encryption.derivationPath,
|
||||
isEthereum = chain.isEthereumBased
|
||||
)
|
||||
}
|
||||
|
||||
fun createRandomGiftSeed(): ByteArray {
|
||||
return SeedCreator.randomSeed(sizeBytes = GIFT_SEED_SIZE_BYTES)
|
||||
}
|
||||
|
||||
private fun getGiftSeedHash(seed: ByteArray): ByteArray {
|
||||
val saltBytes = GIFT_SALT.toByteArray()
|
||||
return SCrypt.generate(seed, saltBytes, N, r, p, SCRYPT_KEY_SIZE)
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain
|
||||
|
||||
import io.novafoundation.nova.common.utils.flowOfAll
|
||||
import io.novafoundation.nova.common.utils.isZero
|
||||
import io.novafoundation.nova.common.utils.onEachAsync
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_gift_impl.data.GiftsRepository
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.Gift
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.isClaimed
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chainWithAsset
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlin.collections.sortedBy
|
||||
|
||||
interface GiftsInteractor {
|
||||
fun observeGifts(fullChainAssetId: FullChainAssetId?): Flow<List<Gift>>
|
||||
|
||||
suspend fun syncGiftsState()
|
||||
}
|
||||
|
||||
class RealGiftsInteractor(
|
||||
private val giftsRepository: GiftsRepository,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase
|
||||
) : GiftsInteractor {
|
||||
|
||||
override fun observeGifts(fullChainAssetId: FullChainAssetId?): Flow<List<Gift>> = flowOfAll {
|
||||
val selectedMetaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
||||
|
||||
giftsRepository.observeGifts()
|
||||
.map { gifts ->
|
||||
gifts.filter { it.creatorMetaId == selectedMetaAccount.id }
|
||||
.filter { it.filterByAsset(fullChainAssetId) }
|
||||
.sortedByDescending { it.creationDate.time }
|
||||
.sortedBy { it.status.isClaimed() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Gift.filterByAsset(fullChainAssetId: FullChainAssetId?): Boolean {
|
||||
if (fullChainAssetId == null) return true
|
||||
|
||||
return chainId == fullChainAssetId.chainId && assetId == fullChainAssetId.assetId
|
||||
}
|
||||
|
||||
override suspend fun syncGiftsState() {
|
||||
giftsRepository.getGifts()
|
||||
.filter { it.status == Gift.Status.PENDING }
|
||||
.onEachAsync {
|
||||
val (chain, chainAsset) = chainRegistry.chainWithAsset(it.chainId, it.assetId)
|
||||
val balanceSource = assetSourceRegistry.sourceFor(chainAsset).balance
|
||||
val giftBalance = balanceSource.queryAccountBalance(chain, chainAsset, it.giftAccountId)
|
||||
|
||||
if (giftBalance.total.isZero) {
|
||||
giftsRepository.setGiftState(it.id, Gift.Status.CLAIMED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.data.memory.ComputationalCache
|
||||
import io.novafoundation.nova.common.utils.flowOf
|
||||
import io.novafoundation.nova.common.utils.mapNotNullToSet
|
||||
import io.novafoundation.nova.common.utils.mapToSet
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency.Asset.Companion.toFeePaymentCurrency
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.fastLookupCustomFeeCapabilityOrDefault
|
||||
import io.novafoundation.nova.feature_gift_api.domain.AvailableGiftAssetsUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.isSelfSufficientAsset
|
||||
import io.novafoundation.nova.runtime.ext.fullId
|
||||
import io.novafoundation.nova.runtime.ext.getAssetOrThrow
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.runningFold
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RealAvailableGiftAssetsUseCase(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val computationalCache: ComputationalCache,
|
||||
private val feePaymentRegistry: FeePaymentProviderRegistry,
|
||||
private val feePaymentFacade: CustomFeeCapabilityFacade,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
) : AvailableGiftAssetsUseCase {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val GIFT_ASSETS_CACHE = "AssetSearchUseCase.GIFT_ASSETS_CACHE"
|
||||
}
|
||||
|
||||
override suspend fun isGiftsAvailable(chainAsset: Chain.Asset): Boolean {
|
||||
return withContext(Dispatchers.Default) {
|
||||
val canPayFee = feePaymentFacade.canPayFeeInCurrency(chainAsset.toFeePaymentCurrency())
|
||||
val isSelfSufficient = assetSourceRegistry.isSelfSufficientAsset(chainAsset)
|
||||
|
||||
canPayFee && isSelfSufficient
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAvailableGiftAssets(coroutineScope: CoroutineScope): Flow<Set<FullChainAssetId>> {
|
||||
return computationalCache.useSharedFlow(GIFT_ASSETS_CACHE, coroutineScope) {
|
||||
flow {
|
||||
// Fast first emission - show all native assets
|
||||
emit(chainRegistry.allNativeAssetIds())
|
||||
|
||||
if (feePaymentFacade.hasGlobalFeePaymentRestrictions()) return@flow
|
||||
|
||||
// Then do the full scan - via slower fee capability check
|
||||
emitPerChainAvailableAssets()
|
||||
}
|
||||
.runningFold(emptySet<FullChainAssetId>()) { acc, newAssets -> if (newAssets.isEmpty()) acc else acc + newAssets }
|
||||
.distinctUntilChangedBy { it.size } // we are only adding so deduplication by size is enough
|
||||
.onEach { Log.d("AssetSearchUseCase", "# of assets available for gifts: ${it.size}") }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ChainRegistry.allNativeAssetIds(): Set<FullChainAssetId> {
|
||||
return currentChains.first().mapToSet { it.utilityAsset.fullId }
|
||||
}
|
||||
|
||||
context(FlowCollector<Set<FullChainAssetId>>)
|
||||
private suspend fun emitPerChainAvailableAssets() {
|
||||
val chains = chainRegistry.currentChains.first()
|
||||
chains.map { chain -> flowOf { collectAllAssetsAllowedForGiftsInChain(chain) } }
|
||||
.merge()
|
||||
.collect { emit(it) }
|
||||
}
|
||||
|
||||
private suspend fun collectAllAssetsAllowedForGiftsInChain(chain: Chain): Set<FullChainAssetId> {
|
||||
val canBeUsedForFeePayment = feePaymentRegistry
|
||||
.providerFor(chain.id)
|
||||
.fastLookupCustomFeeCapabilityOrDefault()
|
||||
.nonUtilityFeeCapableTokens
|
||||
|
||||
return canBeUsedForFeePayment.mapNotNullToSet {
|
||||
val asset = chain.getAssetOrThrow(it)
|
||||
val isSufficient = assetSourceRegistry.isSelfSufficientAsset(asset)
|
||||
if (isSufficient) {
|
||||
asset.fullId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.LightMetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.asMultisig
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.isThreshold1
|
||||
import io.novafoundation.nova.feature_gift_api.domain.GiftsAccountSupportedUseCase
|
||||
import io.novafoundation.nova.feature_gift_api.domain.GiftsSupportedState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class RealGiftsAccountSupportedUseCase(
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
) : GiftsAccountSupportedUseCase {
|
||||
|
||||
override suspend fun supportedState(): GiftsSupportedState {
|
||||
val selectedAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
||||
return selectedAccount.supportsGifts()
|
||||
}
|
||||
|
||||
override fun areGiftsSupportedFlow(): Flow<GiftsSupportedState> {
|
||||
return selectedAccountUseCase.selectedMetaAccountFlow()
|
||||
.map { it.supportsGifts() }
|
||||
}
|
||||
|
||||
private fun MetaAccount.supportsGifts(): GiftsSupportedState {
|
||||
return when (type) {
|
||||
LightMetaAccount.Type.SECRETS,
|
||||
LightMetaAccount.Type.WATCH_ONLY,
|
||||
LightMetaAccount.Type.PARITY_SIGNER,
|
||||
LightMetaAccount.Type.LEDGER_LEGACY,
|
||||
LightMetaAccount.Type.LEDGER,
|
||||
LightMetaAccount.Type.PROXIED,
|
||||
LightMetaAccount.Type.POLKADOT_VAULT -> GiftsSupportedState.SUPPORTED
|
||||
|
||||
LightMetaAccount.Type.MULTISIG -> if (asMultisig().isThreshold1()) {
|
||||
GiftsSupportedState.SUPPORTED
|
||||
} else {
|
||||
GiftsSupportedState.UNSUPPORTED_MULTISIG_ACCOUNTS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain
|
||||
|
||||
import io.novafoundation.nova.feature_gift_impl.data.GiftSecretsRepository
|
||||
import io.novafoundation.nova.feature_gift_impl.data.GiftsRepository
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.Gift
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ShareGiftInteractor {
|
||||
|
||||
fun observeGift(giftId: Long): Flow<Gift>
|
||||
|
||||
suspend fun getGiftSeed(giftId: Long): String
|
||||
|
||||
suspend fun setGiftStateAsReclaimed(id: Long)
|
||||
}
|
||||
|
||||
class RealShareGiftInteractor(
|
||||
private val giftsRepository: GiftsRepository,
|
||||
private val giftSecretsRepository: GiftSecretsRepository,
|
||||
) : ShareGiftInteractor {
|
||||
|
||||
override fun observeGift(giftId: Long): Flow<Gift> {
|
||||
return giftsRepository.observeGift(giftId)
|
||||
}
|
||||
|
||||
override suspend fun getGiftSeed(giftId: Long): String {
|
||||
val gift = giftsRepository.getGift(giftId)
|
||||
val seed = giftSecretsRepository.getGiftAccountSeed(gift.giftAccountId) ?: error("No secrets for gift found")
|
||||
return seed.toHexString()
|
||||
}
|
||||
|
||||
override suspend fun setGiftStateAsReclaimed(id: Long) {
|
||||
giftsRepository.setGiftState(id, Gift.Status.RECLAIMED)
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain.models
|
||||
|
||||
import io.novafoundation.nova.common.data.secrets.v2.ChainAccountSecrets
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.scale.EncodableStruct
|
||||
|
||||
class ClaimableGift(
|
||||
val accountId: AccountId,
|
||||
val chain: Chain,
|
||||
val chainAsset: Chain.Asset,
|
||||
val secrets: EncodableStruct<ChainAccountSecrets>
|
||||
)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain.models
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class CreateGiftModel(
|
||||
val senderMetaAccount: MetaAccount,
|
||||
val chain: Chain,
|
||||
val chainAsset: Chain.Asset,
|
||||
val amount: BigDecimal
|
||||
)
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain.models
|
||||
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.Gift.Status.PENDING
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import java.math.BigInteger
|
||||
import java.util.Date
|
||||
|
||||
class Gift(
|
||||
val id: Long,
|
||||
val amount: BigInteger,
|
||||
val creatorMetaId: Long,
|
||||
val chainId: ChainId,
|
||||
val assetId: ChainAssetId,
|
||||
val status: Status,
|
||||
val giftAccountId: ByteArray,
|
||||
val creationDate: Date
|
||||
) {
|
||||
enum class Status {
|
||||
PENDING,
|
||||
CLAIMED,
|
||||
RECLAIMED
|
||||
}
|
||||
}
|
||||
|
||||
fun Gift.Status.isClaimed() = this != PENDING
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain.models
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
|
||||
import java.math.BigDecimal
|
||||
|
||||
class GiftAmountWithFee(
|
||||
val amount: BigDecimal,
|
||||
val fee: SubmissionFee
|
||||
)
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.domain.models
|
||||
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.feature_account_api.data.model.FeeBase
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
|
||||
import io.novafoundation.nova.feature_account_api.data.model.getAmount
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.addressInput.maxAction.MaxAvailableDeduction
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigInteger
|
||||
|
||||
data class GiftFee(
|
||||
val createGiftFee: SubmissionFee,
|
||||
val claimGiftFee: SubmissionFee
|
||||
) : FeeBase, MaxAvailableDeduction {
|
||||
|
||||
fun replaceSubmission(newSubmissionFee: SubmissionFee): GiftFee {
|
||||
return copy(createGiftFee = newSubmissionFee)
|
||||
}
|
||||
|
||||
override fun maxAmountDeductionFor(amountAsset: Chain.Asset): BigInteger {
|
||||
val createFeeAmount = createGiftFee.getAmount(amountAsset).orZero()
|
||||
val claimFeeAmount = claimGiftFee.getAmount(amountAsset).orZero()
|
||||
|
||||
return createFeeAmount + claimFeeAmount
|
||||
}
|
||||
|
||||
override val amount: BigInteger = createGiftFee.amount + claimGiftFee.amount
|
||||
|
||||
override val asset: Chain.Asset = createGiftFee.asset
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation
|
||||
|
||||
import io.novafoundation.nova.common.navigation.ReturnableRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.GiftId
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.claim.ClaimGiftPayload
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.confirm.CreateGiftConfirmPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
|
||||
interface GiftRouter : ReturnableRouter {
|
||||
|
||||
fun finishCreateGift()
|
||||
|
||||
fun openGiftsFlow()
|
||||
|
||||
fun openSelectGiftAmount(assetPayload: AssetPayload)
|
||||
|
||||
fun openConfirmCreateGift(payload: CreateGiftConfirmPayload)
|
||||
|
||||
fun openGiftSharing(giftId: GiftId, isSecondOpen: Boolean = false)
|
||||
|
||||
fun openMainScreen()
|
||||
|
||||
fun openClaimGift(claimGiftPayload: ClaimGiftPayload)
|
||||
|
||||
fun openManageWallets()
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.amount
|
||||
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.CreateGiftInteractor
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.common.MinAmountProvider
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigDecimal
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GiftMinAmountProviderFactory(
|
||||
private val createGiftInteractor: CreateGiftInteractor,
|
||||
) {
|
||||
fun create(
|
||||
chainAssetFlow: Flow<Chain.Asset>
|
||||
): MinAmountProvider {
|
||||
return GiftMinAmountProvider(
|
||||
createGiftInteractor,
|
||||
chainAssetFlow
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class GiftMinAmountProvider(
|
||||
private val createGiftInteractor: CreateGiftInteractor,
|
||||
private val chainAssetFlow: Flow<Chain.Asset>
|
||||
) : MinAmountProvider {
|
||||
|
||||
override fun provideMinAmount(): Flow<BigDecimal> {
|
||||
return chainAssetFlow
|
||||
.map { createGiftInteractor.getExistentialDeposit(it) }
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.amount
|
||||
|
||||
import android.view.View
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.mixin.impl.observeValidations
|
||||
import io.novafoundation.nova.common.utils.FragmentPayloadCreator
|
||||
import io.novafoundation.nova.common.utils.PayloadCreator
|
||||
import io.novafoundation.nova.common.utils.hideKeyboard
|
||||
import io.novafoundation.nova.common.utils.insets.ImeInsetsState
|
||||
import io.novafoundation.nova.common.utils.insets.applySystemBarInsets
|
||||
import io.novafoundation.nova.common.utils.payload
|
||||
import io.novafoundation.nova.common.view.setState
|
||||
import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_impl.databinding.FragmentSelectGiftAmountBinding
|
||||
import io.novafoundation.nova.feature_gift_impl.di.GiftFeatureComponent
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.setupAmountChooser
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.setupFeeLoading
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.getAsset.bindGetAsset
|
||||
|
||||
class SelectGiftAmountFragment : BaseFragment<SelectGiftAmountViewModel, FragmentSelectGiftAmountBinding>() {
|
||||
|
||||
companion object : PayloadCreator<SelectGiftAmountPayload> by FragmentPayloadCreator()
|
||||
|
||||
override fun createBinding() = FragmentSelectGiftAmountBinding.inflate(layoutInflater)
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.root.applySystemBarInsets(imeInsets = ImeInsetsState.ENABLE_IF_SUPPORTED)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.giftAmountToolbar.setHomeButtonListener {
|
||||
hideKeyboard()
|
||||
viewModel.back()
|
||||
}
|
||||
|
||||
binder.giftAmountContinue.prepareForProgress(this)
|
||||
binder.giftAmountContinue.setOnClickListener { viewModel.nextClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<GiftFeatureComponent>(this, GiftFeatureApi::class.java)
|
||||
.selectGiftAmountComponentFactory()
|
||||
.create(this, payload())
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: SelectGiftAmountViewModel) {
|
||||
observeValidations(viewModel)
|
||||
setupAmountChooser(viewModel.amountChooserMixin, binder.giftsAmount)
|
||||
|
||||
viewModel.feeMixin.setupFeeLoading(binder.giftAmountFee)
|
||||
|
||||
viewModel.getAssetOptionsMixin.bindGetAsset(binder.giftAmountGetTokens)
|
||||
|
||||
viewModel.chainModelFlow.observe { binder.giftAmountChain.setModel(it) }
|
||||
|
||||
viewModel.continueButtonStateFlow.observe(binder.giftAmountContinue::setState)
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.amount
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class SelectGiftAmountPayload(val assetPayload: AssetPayload) : Parcelable
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.amount
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.mixin.api.Validatable
|
||||
import io.novafoundation.nova.common.presentation.DescriptiveButtonState
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.isPositive
|
||||
import io.novafoundation.nova.common.utils.isZero
|
||||
import io.novafoundation.nova.common.utils.launchUnit
|
||||
import io.novafoundation.nova.common.utils.orZero
|
||||
import io.novafoundation.nova.common.validation.CompoundFieldValidator
|
||||
import io.novafoundation.nova.common.validation.FieldValidationResult
|
||||
import io.novafoundation.nova.common.validation.FieldValidator
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.common.validation.isErrorWithTag
|
||||
import io.novafoundation.nova.common.validation.progressConsumer
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.view.ChainChipModel
|
||||
import io.novafoundation.nova.feature_gift_impl.R
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.CreateGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.CreateGiftModel
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.GiftFee
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.amount.fee.createForGiftsWithDefaultDisplay
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.buildGiftValidationPayload
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.confirm.CreateGiftConfirmPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.common.fieldValidator.EnoughAmountFieldValidator
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.common.fieldValidator.EnoughAmountValidatorFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.common.fieldValidator.MinAmountFieldValidatorFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.isMaxAction
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.DefaultFeeFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.awaitFee
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.connectWith
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.getAsset.GetAssetOptionsMixin
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.maxAction.MaxActionProviderFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.maxAction.create
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.validation.transfers.autoFixSendValidationPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.validation.transfers.mapAssetTransferValidationFailureToUI
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chainFlow
|
||||
import java.math.BigDecimal
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SelectGiftAmountViewModel(
|
||||
private val router: GiftRouter,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val validationExecutor: ValidationExecutor,
|
||||
private val assetUseCase: ArbitraryAssetUseCase,
|
||||
private val payload: SelectGiftAmountPayload,
|
||||
private val maxActionProviderFactory: MaxActionProviderFactory,
|
||||
private val amountFormatter: AmountFormatter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val getAssetOptionsMixinFactory: GetAssetOptionsMixin.Factory,
|
||||
private val createGiftInteractor: CreateGiftInteractor,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
private val enoughAmountValidatorFactory: EnoughAmountValidatorFactory,
|
||||
private val minAmountFieldValidatorFactory: MinAmountFieldValidatorFactory,
|
||||
private val giftMinAmountProviderFactory: GiftMinAmountProviderFactory,
|
||||
amountChooserMixinFactory: AmountChooserMixin.Factory,
|
||||
feeLoaderMixinFactory: FeeLoaderMixinV2.Factory,
|
||||
) : BaseViewModel(),
|
||||
Validatable by validationExecutor {
|
||||
|
||||
private val chainFlow = chainRegistry.chainFlow(payload.assetPayload.chainId)
|
||||
private val chainAssetFlow = chainFlow.map { it.assetsById.getValue(payload.assetPayload.chainAssetId) }
|
||||
|
||||
private val metaAccountFlow = selectedAccountUseCase.selectedMetaAccountFlow()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val assetFlow = chainAssetFlow.flatMapLatest(assetUseCase::assetFlow)
|
||||
.shareInBackground()
|
||||
|
||||
val chainModelFlow = chainFlow.map {
|
||||
ChainChipModel(
|
||||
chainUi = mapChainToUi(it),
|
||||
changeable = false
|
||||
)
|
||||
}.shareInBackground()
|
||||
|
||||
private val feeFormatter = DefaultFeeFormatter<GiftFee>(amountFormatter)
|
||||
val feeMixin = feeLoaderMixinFactory.createForGiftsWithDefaultDisplay(
|
||||
chainAssetFlow,
|
||||
feeFormatter
|
||||
)
|
||||
|
||||
private val maxActionProvider = maxActionProviderFactory.create(
|
||||
viewModelScope = viewModelScope,
|
||||
assetInFlow = assetFlow,
|
||||
feeLoaderMixin = feeMixin
|
||||
)
|
||||
|
||||
val amountChooserMixin: AmountChooserMixin.Presentation = amountChooserMixinFactory.create(
|
||||
scope = this,
|
||||
assetFlow = assetFlow,
|
||||
maxActionProvider = maxActionProvider,
|
||||
fieldValidator = getAmountValidator()
|
||||
)
|
||||
|
||||
private val notEnoughAmountErrorFlow = combine(assetFlow, amountChooserMixin.fieldError) { asset, fieldError ->
|
||||
asset.transferable.isZero || fieldError.isErrorWithTag(EnoughAmountFieldValidator.ERROR_TAG)
|
||||
}
|
||||
val getAssetOptionsMixin = getAssetOptionsMixinFactory.create(
|
||||
assetFlow = chainAssetFlow,
|
||||
additionalButtonFilter = notEnoughAmountErrorFlow,
|
||||
scope = viewModelScope,
|
||||
)
|
||||
|
||||
private val validationInProgressFlow = MutableStateFlow(false)
|
||||
|
||||
val continueButtonStateFlow = combine(
|
||||
validationInProgressFlow,
|
||||
amountChooserMixin.fieldError,
|
||||
amountChooserMixin.amountState
|
||||
) { validating, fieldError, amountState ->
|
||||
when {
|
||||
validating -> DescriptiveButtonState.Loading
|
||||
fieldError is FieldValidationResult.Error -> DescriptiveButtonState.Disabled(resourceManager.getString(R.string.common_enter_other_amount))
|
||||
amountState.value.orZero().isPositive -> DescriptiveButtonState.Enabled(resourceManager.getString(R.string.common_continue))
|
||||
else -> DescriptiveButtonState.Disabled(resourceManager.getString(R.string.gift_enter_amount_disabled_button_state))
|
||||
}
|
||||
}.onStart { emit(DescriptiveButtonState.Disabled(resourceManager.getString(R.string.gift_enter_amount_disabled_button_state))) }
|
||||
|
||||
init {
|
||||
setupFees()
|
||||
}
|
||||
|
||||
fun back() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun nextClicked() = launchUnit {
|
||||
validationInProgressFlow.value = true
|
||||
val fee = feeMixin.awaitFee()
|
||||
val amountState = amountChooserMixin.amountState.first()
|
||||
val giftAmount = amountState.value ?: return@launchUnit
|
||||
val chain = chainFlow.first()
|
||||
|
||||
val giftModel = CreateGiftModel(
|
||||
senderMetaAccount = selectedAccountUseCase.getSelectedMetaAccount(),
|
||||
chain = chain,
|
||||
chainAsset = chainAssetFlow.first(),
|
||||
amount = giftAmount,
|
||||
)
|
||||
|
||||
val payload = buildGiftValidationPayload(
|
||||
giftModel,
|
||||
asset = assetFlow.first(),
|
||||
amountState.inputKind.isMaxAction(),
|
||||
feeMixin.feePaymentCurrency(),
|
||||
fee
|
||||
)
|
||||
|
||||
validationExecutor.requireValid(
|
||||
validationSystem = createGiftInteractor.validationSystemFor(giftModel.chainAsset, viewModelScope),
|
||||
payload = payload,
|
||||
progressConsumer = validationInProgressFlow.progressConsumer(),
|
||||
autoFixPayload = ::autoFixSendValidationPayload,
|
||||
validationFailureTransformerCustom = { status, actions ->
|
||||
viewModelScope.mapAssetTransferValidationFailureToUI(
|
||||
resourceManager = resourceManager,
|
||||
status = status,
|
||||
actions = actions,
|
||||
setFee = { feeMixin.setFee(fee.replaceSubmission(it)) }
|
||||
)
|
||||
},
|
||||
) {
|
||||
validationInProgressFlow.value = false
|
||||
|
||||
openConfirmScreen(it, giftAmount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openConfirmScreen(validPayload: AssetTransferPayload, giftAmount: BigDecimal) = launch {
|
||||
val payload = CreateGiftConfirmPayload(
|
||||
amount = giftAmount,
|
||||
transferringMaxAmount = validPayload.transfer.transferringMaxAmount,
|
||||
assetPayload = payload.assetPayload
|
||||
)
|
||||
|
||||
router.openConfirmCreateGift(payload)
|
||||
}
|
||||
|
||||
private fun setupFees() {
|
||||
feeMixin.connectWith(
|
||||
chainFlow,
|
||||
chainAssetFlow,
|
||||
amountChooserMixin.amountState,
|
||||
) { feePaymentCurrency, chain, chainAsset, amountState ->
|
||||
val metaAccount = metaAccountFlow.first()
|
||||
val createGiftModel = CreateGiftModel(
|
||||
senderMetaAccount = metaAccount,
|
||||
chain = chain,
|
||||
chainAsset = chainAsset,
|
||||
amount = amountState.value.orZero(),
|
||||
)
|
||||
|
||||
createGiftInteractor.getFee(
|
||||
createGiftModel,
|
||||
amountState.inputKind.isMaxAction(),
|
||||
viewModelScope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAmountValidator(): FieldValidator {
|
||||
val minAmountProvider = giftMinAmountProviderFactory.create(chainAssetFlow)
|
||||
|
||||
return CompoundFieldValidator(
|
||||
enoughAmountValidatorFactory.create(maxActionProvider),
|
||||
minAmountFieldValidatorFactory.create(chainAssetFlow, minAmountProvider, R.string.gift_min_balance_validation_message)
|
||||
)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.amount.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_gift_impl.presentation.amount.SelectGiftAmountFragment
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.amount.SelectGiftAmountPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
SelectGiftAmountModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface SelectGiftAmountComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: SelectGiftAmountPayload
|
||||
): SelectGiftAmountComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: SelectGiftAmountFragment)
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.amount.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.CreateGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.amount.GiftMinAmountProviderFactory
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.amount.SelectGiftAmountPayload
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.amount.SelectGiftAmountViewModel
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.common.fieldValidator.EnoughAmountValidatorFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.common.fieldValidator.MinAmountFieldValidatorFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.amountChooser.AmountChooserMixin
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.getAsset.GetAssetOptionsMixin
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.maxAction.MaxActionProviderFactory
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class SelectGiftAmountModule {
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): SelectGiftAmountViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(SelectGiftAmountViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(SelectGiftAmountViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: GiftRouter,
|
||||
chainRegistry: ChainRegistry,
|
||||
validationExecutor: ValidationExecutor,
|
||||
assetUseCase: ArbitraryAssetUseCase,
|
||||
payload: SelectGiftAmountPayload,
|
||||
maxActionProviderFactory: MaxActionProviderFactory,
|
||||
amountFormatter: AmountFormatter,
|
||||
resourceManager: ResourceManager,
|
||||
getAssetOptionsMixinFactory: GetAssetOptionsMixin.Factory,
|
||||
createGiftInteractor: CreateGiftInteractor,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
enoughAmountValidatorFactory: EnoughAmountValidatorFactory,
|
||||
minAmountFieldValidatorFactory: MinAmountFieldValidatorFactory,
|
||||
giftMinAmountProviderFactory: GiftMinAmountProviderFactory,
|
||||
amountChooserMixinFactory: AmountChooserMixin.Factory,
|
||||
feeLoaderMixinFactory: FeeLoaderMixinV2.Factory,
|
||||
): ViewModel {
|
||||
return SelectGiftAmountViewModel(
|
||||
router = router,
|
||||
chainRegistry = chainRegistry,
|
||||
validationExecutor = validationExecutor,
|
||||
assetUseCase = assetUseCase,
|
||||
payload = payload,
|
||||
maxActionProviderFactory = maxActionProviderFactory,
|
||||
amountFormatter = amountFormatter,
|
||||
resourceManager = resourceManager,
|
||||
getAssetOptionsMixinFactory = getAssetOptionsMixinFactory,
|
||||
createGiftInteractor = createGiftInteractor,
|
||||
selectedAccountUseCase = selectedAccountUseCase,
|
||||
enoughAmountValidatorFactory = enoughAmountValidatorFactory,
|
||||
minAmountFieldValidatorFactory = minAmountFieldValidatorFactory,
|
||||
giftMinAmountProviderFactory = giftMinAmountProviderFactory,
|
||||
amountChooserMixinFactory = amountChooserMixinFactory,
|
||||
feeLoaderMixinFactory = feeLoaderMixinFactory,
|
||||
)
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.amount.fee
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay
|
||||
|
||||
class GiftFeeDisplay(
|
||||
val networkFee: FeeDisplay,
|
||||
val claimGiftFee: FeeDisplay,
|
||||
)
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.amount.fee
|
||||
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.GiftFee
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.model.AmountConfig
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.FeeFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeStatus
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.toFeeDisplay
|
||||
|
||||
class GiftFeeDisplayFormatter(
|
||||
private val amountFormatter: AmountFormatter
|
||||
) : FeeFormatter<GiftFee, GiftFeeDisplay> {
|
||||
|
||||
override suspend fun formatFee(
|
||||
fee: GiftFee,
|
||||
configuration: FeeFormatter.Configuration,
|
||||
context: FeeFormatter.Context
|
||||
): GiftFeeDisplay {
|
||||
val networkFee = amountFormatter.formatAmountToAmountModel(
|
||||
amountInPlanks = fee.createGiftFee.amount,
|
||||
token = context.token(fee.asset),
|
||||
AmountConfig(includeZeroFiat = configuration.showZeroFiat)
|
||||
).toFeeDisplay()
|
||||
|
||||
val claimFee = amountFormatter.formatAmountToAmountModel(
|
||||
amountInPlanks = fee.claimGiftFee.amount,
|
||||
token = context.token(fee.asset),
|
||||
AmountConfig(includeZeroFiat = configuration.showZeroFiat)
|
||||
).toFeeDisplay()
|
||||
|
||||
return GiftFeeDisplay(
|
||||
networkFee = networkFee,
|
||||
claimGiftFee = claimFee
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun createLoadingStatus(): FeeStatus.Loading {
|
||||
return FeeStatus.Loading(visibleDuringProgress = true)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.amount.fee
|
||||
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.GiftFee
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.amount.FeeInspector
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
class GiftFeeInspector : FeeInspector<GiftFee> {
|
||||
|
||||
override fun inspectFeeAmount(fee: GiftFee): FeeInspector.InspectedFeeAmount {
|
||||
return FeeInspector.InspectedFeeAmount(
|
||||
checkedAgainstMinimumBalance = fee.amount,
|
||||
deductedFromTransferable = fee.amount
|
||||
)
|
||||
}
|
||||
|
||||
override fun getSubmissionFeeAsset(fee: GiftFee): Chain.Asset {
|
||||
return fee.createGiftFee.asset
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.amount.fee
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.GiftFee
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.formatter.DefaultFeeFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.FeeDisplay
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.asFeeContextFromSelf
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
context(BaseViewModel)
|
||||
fun FeeLoaderMixinV2.Factory.createForGiftsWithDefaultDisplay(
|
||||
originChainAsset: Flow<Chain.Asset>,
|
||||
formatter: DefaultFeeFormatter<GiftFee>
|
||||
): FeeLoaderMixinV2.Presentation<GiftFee, FeeDisplay> {
|
||||
return create(
|
||||
scope = viewModelScope,
|
||||
feeContextFlow = originChainAsset.asFeeContextFromSelf(),
|
||||
feeFormatter = formatter,
|
||||
feeInspector = GiftFeeInspector(),
|
||||
configuration = FeeLoaderMixinV2.Configuration()
|
||||
)
|
||||
}
|
||||
|
||||
context(BaseViewModel)
|
||||
fun FeeLoaderMixinV2.Factory.createForGiftsWithGiftFeeDisplay(
|
||||
originChainAsset: Flow<Chain.Asset>,
|
||||
formatter: GiftFeeDisplayFormatter
|
||||
): FeeLoaderMixinV2.Presentation<GiftFee, GiftFeeDisplay> {
|
||||
return create(
|
||||
scope = viewModelScope,
|
||||
feeContextFlow = originChainAsset.asFeeContextFromSelf(),
|
||||
feeFormatter = formatter,
|
||||
feeInspector = GiftFeeInspector(),
|
||||
configuration = FeeLoaderMixinV2.Configuration()
|
||||
)
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.claim
|
||||
|
||||
import android.animation.Animator
|
||||
import android.view.View
|
||||
import androidx.core.view.postDelayed
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.FragmentPayloadCreator
|
||||
import io.novafoundation.nova.common.utils.PayloadCreator
|
||||
import io.novafoundation.nova.common.utils.makeInvisible
|
||||
import io.novafoundation.nova.common.utils.payload
|
||||
import io.novafoundation.nova.common.view.setModelOrHide
|
||||
import io.novafoundation.nova.common.view.setState
|
||||
import io.novafoundation.nova.feature_account_api.R
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectSingleWallet.bindSelectWallet
|
||||
import io.novafoundation.nova.feature_account_api.view.setSelectable
|
||||
import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_impl.databinding.FragmentClaimGiftBinding
|
||||
import io.novafoundation.nova.feature_gift_impl.di.GiftFeatureComponent
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val HIDE_ANIMATION_DURATION = 400L
|
||||
private const val UNPACKING_START_FRAME = 180
|
||||
|
||||
class ClaimGiftFragment : BaseFragment<ClaimGiftViewModel, FragmentClaimGiftBinding>() {
|
||||
|
||||
companion object : PayloadCreator<ClaimGiftPayload> by FragmentPayloadCreator()
|
||||
|
||||
private val giftAnimationListener = object : Animator.AnimatorListener {
|
||||
override fun onAnimationCancel(animation: Animator) {}
|
||||
override fun onAnimationRepeat(animation: Animator) {}
|
||||
override fun onAnimationStart(animation: Animator) {}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
viewModel.onGiftClaimAnimationFinished()
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
override fun createBinding() = FragmentClaimGiftBinding.inflate(layoutInflater)
|
||||
|
||||
override fun initViews() {
|
||||
binder.claimGiftToolbar.setHomeButtonListener { viewModel.back() }
|
||||
|
||||
binder.claimGiftButton.setOnClickListener { viewModel.claimGift() }
|
||||
binder.claimGiftButton.prepareForProgress(this)
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<GiftFeatureComponent>(this, GiftFeatureApi::class.java)
|
||||
.claimGiftComponentFactory()
|
||||
.create(this, payload())
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: ClaimGiftViewModel) {
|
||||
bindSelectWallet(viewModel.selectWalletMixin) { isAvailableToSelect ->
|
||||
binder.claimGiftAccount.setSelectable(isAvailableToSelect) {
|
||||
viewModel.selectWalletToClaim()
|
||||
}
|
||||
binder.claimGiftAccount.setActionTint(R.color.icon_secondary)
|
||||
}
|
||||
|
||||
viewModel.giftAnimationRes.observe {
|
||||
binder.claimGiftAnimation.setMinAndMaxFrame(0, UNPACKING_START_FRAME)
|
||||
binder.claimGiftAnimation.setAnimation(it)
|
||||
binder.claimGiftAnimation.playAnimation()
|
||||
}
|
||||
|
||||
viewModel.amountModel.observe {
|
||||
binder.claimGiftTokenIcon.setTokenIcon(it.tokenIcon, imageLoader)
|
||||
binder.claimGiftAmount.text = it.amount
|
||||
}
|
||||
|
||||
viewModel.giftClaimedEvent.observeEvent {
|
||||
hideAllViewsWithAnimation()
|
||||
}
|
||||
|
||||
viewModel.selectedWalletModel.observe {
|
||||
binder.claimGiftAccount.setModel(it)
|
||||
}
|
||||
|
||||
viewModel.confirmButtonStateFlow.observe {
|
||||
binder.claimGiftButton.setState(it)
|
||||
}
|
||||
|
||||
viewModel.alertModelFlow.observe {
|
||||
binder.claimGiftAlert.setModelOrHide(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideAllViewsWithAnimation() {
|
||||
binder.claimGiftToolbar.hideWithAnimation()
|
||||
binder.claimGiftTokenIcon.hideWithAnimation()
|
||||
binder.claimGiftAmount.hideWithAnimation()
|
||||
binder.claimGiftButton.hideWithAnimation()
|
||||
binder.claimGiftTitle.hideWithAnimation()
|
||||
binder.claimGiftAccountTitle.hideWithAnimation()
|
||||
binder.claimGiftAccount.hideWithAnimation()
|
||||
|
||||
binder.root.postDelayed(HIDE_ANIMATION_DURATION) {
|
||||
val maxFrame = binder.claimGiftAnimation.composition?.endFrame?.toInt() ?: 0
|
||||
binder.claimGiftAnimation.setMinAndMaxFrame(UNPACKING_START_FRAME, maxFrame)
|
||||
binder.claimGiftAnimation.addAnimatorListener(giftAnimationListener)
|
||||
binder.claimGiftAnimation.playAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
private fun View.hideWithAnimation() {
|
||||
animate().alpha(0f)
|
||||
.setDuration(HIDE_ANIMATION_DURATION)
|
||||
.withEndAction { makeInvisible() }
|
||||
.start()
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.claim
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class ClaimGiftPayload(val secret: ByteArray, val chainId: String, val assetId: Int) : Parcelable
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.claim
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.address.AddressModel
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
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.common.utils.launchUnit
|
||||
import io.novafoundation.nova.common.view.AlertModel
|
||||
import io.novafoundation.nova.common.view.AlertView
|
||||
import io.novafoundation.nova.feature_account_api.domain.filter.selectAddress.SelectAccountFilter
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountInteractor
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.isControllableWallet
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.common.mapMetaAccountTypeToNameRes
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectSingleWallet.SelectSingleWalletMixin
|
||||
import io.novafoundation.nova.feature_account_api.view.AccountView
|
||||
import io.novafoundation.nova.feature_gift_impl.R
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.ClaimGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.claim.ClaimGiftMixinFactory
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.UnpackingGiftAnimationFactory
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.claim.ClaimGiftException
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.share.model.GiftAmountModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.TokenFormatter
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ClaimGiftViewModel(
|
||||
private val router: GiftRouter,
|
||||
private val payload: ClaimGiftPayload,
|
||||
private val claimGiftInteractor: ClaimGiftInteractor,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val unpackingGiftAnimationFactory: UnpackingGiftAnimationFactory,
|
||||
private val assetIconProvider: AssetIconProvider,
|
||||
private val tokenFormatter: TokenFormatter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val walletUiUseCase: WalletUiUseCase,
|
||||
private val claimGiftMixinFactory: ClaimGiftMixinFactory,
|
||||
private val accountInteractor: AccountInteractor,
|
||||
selectSingleWalletMixin: SelectSingleWalletMixin.Factory,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val giftFlow = flowOf { claimGiftInteractor.getClaimableGift(payload.secret, payload.chainId, payload.assetId) }
|
||||
.shareInBackground()
|
||||
|
||||
private val metaIdToClaimGiftFlow = MutableStateFlow<Long?>(null)
|
||||
|
||||
private val metaAccountToClaimGiftFlow = metaIdToClaimGiftFlow.filterNotNull()
|
||||
.map { claimGiftInteractor.getMetaAccount(it) }
|
||||
.shareInBackground()
|
||||
|
||||
private val tempMetaAccountFlow = giftFlow.map { claimGiftInteractor.createTempMetaAccount(it) }
|
||||
.shareInBackground()
|
||||
|
||||
private val giftAmountWithFee = combine(giftFlow, tempMetaAccountFlow) { gift, metaAccount ->
|
||||
claimGiftInteractor.getGiftAmountWithFee(gift, metaAccount, coroutineScope)
|
||||
}.shareInBackground()
|
||||
|
||||
val selectedWalletModel = combine(giftFlow, metaAccountToClaimGiftFlow) { gift, metaAccountToClaimGift ->
|
||||
val addressModel = walletUiUseCase.walletAddressModelOrNull(
|
||||
metaAccountToClaimGift,
|
||||
gift.chain,
|
||||
AddressIconGenerator.SIZE_MEDIUM
|
||||
)
|
||||
|
||||
addressModel.asAccountViewModelOrNoAddress(metaAccountToClaimGift, gift.chain)
|
||||
}
|
||||
|
||||
val amountModel = combine(giftFlow, giftAmountWithFee) { gift, giftAmountWithFee ->
|
||||
val tokenIcon = assetIconProvider.getAssetIconOrFallback(gift.chainAsset)
|
||||
val giftAmount = tokenFormatter.formatToken(giftAmountWithFee.amount, gift.chainAsset.symbol)
|
||||
GiftAmountModel(tokenIcon, giftAmount)
|
||||
}
|
||||
|
||||
private val _giftClaimedEvent = MutableLiveData<Event<Unit>>()
|
||||
val giftClaimedEvent: LiveData<Event<Unit>> = _giftClaimedEvent
|
||||
|
||||
val giftAnimationRes = giftFlow.map {
|
||||
val chainAsset = chainRegistry.asset(it.chain.id, it.chainAsset.id)
|
||||
unpackingGiftAnimationFactory.getAnimationForAsset(chainAsset.symbol)
|
||||
}.distinctUntilChanged()
|
||||
.shareInBackground()
|
||||
|
||||
private val selectWalletPayloadFlow = giftFlow.map {
|
||||
SelectSingleWalletMixin.Payload(
|
||||
chain = it.chain,
|
||||
filter = SelectAccountFilter.ControllableWallets()
|
||||
)
|
||||
}
|
||||
val selectWalletMixin = selectSingleWalletMixin.create(
|
||||
coroutineScope = this,
|
||||
payloadFlow = selectWalletPayloadFlow,
|
||||
onWalletSelect = ::onWalletSelect
|
||||
)
|
||||
|
||||
val alertModelFlow = combine(giftFlow, metaAccountToClaimGiftFlow) { gift, metaAccount ->
|
||||
when {
|
||||
!metaAccount.type.isControllableWallet() -> {
|
||||
val metaAccountTypeName = resourceManager.getString(metaAccount.type.mapMetaAccountTypeToNameRes())
|
||||
AlertModel(
|
||||
style = AlertView.Style.fromPreset(AlertView.StylePreset.WARNING),
|
||||
message = resourceManager.getString(
|
||||
R.string.claim_gift_uncontrollable_wallet_title,
|
||||
metaAccountTypeName.lowercase()
|
||||
),
|
||||
subMessages = listOf(resourceManager.getString(R.string.claim_gift_uncontrollable_wallet_message)),
|
||||
linkAction = AlertModel.ActionModel(
|
||||
text = resourceManager.getString(R.string.common_manage_wallets),
|
||||
listener = ::manageWallets
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
!metaAccount.hasAccountIn(gift.chain) -> AlertModel(
|
||||
style = AlertView.Style.fromPreset(AlertView.StylePreset.WARNING),
|
||||
message = resourceManager.getString(R.string.claim_gift_no_account_alert_title, gift.chain.name),
|
||||
subMessages = listOf(),
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private val claimGiftMixin = claimGiftMixinFactory.create(this)
|
||||
|
||||
val confirmButtonStateFlow = combine(
|
||||
claimGiftMixin.claimingInProgressFlow,
|
||||
giftFlow,
|
||||
metaAccountToClaimGiftFlow
|
||||
) { claimingInProgress, giftFlow, claimMetaAccount ->
|
||||
when {
|
||||
claimingInProgress -> DescriptiveButtonState.Loading
|
||||
|
||||
!claimMetaAccount.type.isControllableWallet() -> DescriptiveButtonState.Gone
|
||||
|
||||
!claimMetaAccount.hasAccountIn(giftFlow.chain) -> {
|
||||
DescriptiveButtonState.Disabled(resourceManager.getString(R.string.account_select_wallet))
|
||||
}
|
||||
|
||||
else -> DescriptiveButtonState.Enabled(resourceManager.getString(R.string.claim_gift_button))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
launch {
|
||||
val metaAccount = claimGiftInteractor.getMetaAccountToClaimGift()
|
||||
metaIdToClaimGiftFlow.value = metaAccount.id
|
||||
}
|
||||
}
|
||||
|
||||
fun back() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun claimGift() = launchUnit {
|
||||
val gift = giftFlow.first()
|
||||
val amountWithFee = giftAmountWithFee.first()
|
||||
val tempMetaAccount = tempMetaAccountFlow.first()
|
||||
val metaAccountToClaimGift = metaAccountToClaimGiftFlow.first()
|
||||
|
||||
claimGiftMixin.claimGift(
|
||||
gift = gift,
|
||||
amountWithFee = amountWithFee,
|
||||
giftMetaAccount = tempMetaAccount,
|
||||
giftRecipient = metaAccountToClaimGift
|
||||
)
|
||||
.onSuccess {
|
||||
val metaAccountToClaimGift = metaAccountToClaimGiftFlow.first()
|
||||
accountInteractor.selectMetaAccount(metaAccountToClaimGift.id)
|
||||
_giftClaimedEvent.value = Unit.event()
|
||||
}
|
||||
.onFailure {
|
||||
when (it as ClaimGiftException) {
|
||||
is ClaimGiftException.GiftAlreadyClaimed -> showError(
|
||||
resourceManager.getString(R.string.claim_gift_already_claimed_title),
|
||||
resourceManager.getString(R.string.claim_gift_already_claimed_message)
|
||||
)
|
||||
|
||||
is ClaimGiftException.UnknownError -> showError(
|
||||
resourceManager.getString(R.string.claim_gift_default_error_title),
|
||||
resourceManager.getString(R.string.claim_gift_default_error_message)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onGiftClaimAnimationFinished() = launchUnit {
|
||||
showToast(resourceManager.getString(R.string.claim_gift_success_message))
|
||||
|
||||
router.openMainScreen()
|
||||
}
|
||||
|
||||
fun selectWalletToClaim() {
|
||||
launch {
|
||||
val selectedMetaAccount = metaAccountToClaimGiftFlow.first()
|
||||
selectWalletMixin.openSelectWallet(selectedMetaAccount.id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onWalletSelect(metaId: Long) {
|
||||
metaIdToClaimGiftFlow.value = metaId
|
||||
}
|
||||
|
||||
private fun manageWallets() = launchUnit {
|
||||
router.openManageWallets()
|
||||
}
|
||||
|
||||
private fun AddressModel?.asAccountViewModelOrNoAddress(
|
||||
metaAccountToClaimGift: MetaAccount,
|
||||
chain: Chain
|
||||
): AccountView.Model {
|
||||
return this?.let { AccountView.Model.Address(it) }
|
||||
?: AccountView.Model.NoAddress(
|
||||
metaAccountToClaimGift.name,
|
||||
resourceManager.getString(R.string.account_chain_not_found, chain.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.claim.deeplink
|
||||
|
||||
import android.net.Uri
|
||||
import io.novafoundation.nova.common.utils.TokenSymbol
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.DeepLinkConfigurator
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.LinkBuilderFactory
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.addParamIfNotNull
|
||||
import io.novafoundation.nova.runtime.ext.ChainGeneses
|
||||
import kotlin.math.min
|
||||
|
||||
class ClaimGiftDeepLinkData(
|
||||
val seed: String,
|
||||
val chainId: String,
|
||||
val symbol: TokenSymbol
|
||||
)
|
||||
|
||||
class ClaimGiftPayloadParams(
|
||||
val seed: String,
|
||||
val chainIdPrefix: String?,
|
||||
val symbol: TokenSymbol?
|
||||
)
|
||||
|
||||
class ClaimGiftDeepLinkConfigurator(
|
||||
private val linkBuilderFactory: LinkBuilderFactory
|
||||
) : DeepLinkConfigurator<ClaimGiftDeepLinkData> {
|
||||
|
||||
val action = "open"
|
||||
val screen = "gift"
|
||||
val deepLinkPrefix = "/$action/$screen"
|
||||
val payloadParam = "payload"
|
||||
val chainIdLength = 6
|
||||
|
||||
private val defaultChainId = ChainGeneses.POLKADOT_ASSET_HUB
|
||||
private val defaultAssetId = "DOT"
|
||||
|
||||
override fun configure(payload: ClaimGiftDeepLinkData, type: DeepLinkConfigurator.Type): Uri {
|
||||
val data = if (payload.chainId == defaultChainId) {
|
||||
if (payload.symbol.value == defaultAssetId) {
|
||||
payload.seed
|
||||
} else {
|
||||
makePayload(payload.seed, payload.symbol.value)
|
||||
}
|
||||
} else {
|
||||
val normalizedChainId = normaliseChainId(payload.chainId)
|
||||
makePayload(payload.seed, payload.symbol.value, normalizedChainId)
|
||||
}
|
||||
|
||||
return linkBuilderFactory.newLink(type)
|
||||
.setAction(action)
|
||||
.setScreen(screen)
|
||||
.addParamIfNotNull(payloadParam, data)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun normaliseChainId(chainId: String): String {
|
||||
val noPrefixChainId = chainId.removePrefix("eip155:")
|
||||
val substringEnd = min(noPrefixChainId.length, chainIdLength)
|
||||
return noPrefixChainId.substring(startIndex = 0, endIndex = substringEnd)
|
||||
}
|
||||
|
||||
fun fromPayload(payload: String): ClaimGiftPayloadParams {
|
||||
val payloadParams = payload.split("_")
|
||||
val seed = payloadParams[0]
|
||||
val symbol = payloadParams.getOrNull(1)
|
||||
val chainIdPrefix = payloadParams.getOrNull(2)
|
||||
|
||||
return ClaimGiftPayloadParams(
|
||||
seed = seed,
|
||||
symbol = symbol?.let { TokenSymbol(it) },
|
||||
chainIdPrefix = chainIdPrefix
|
||||
)
|
||||
}
|
||||
|
||||
private fun makePayload(vararg params: String): String {
|
||||
return params.joinToString("_")
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.claim.deeplink
|
||||
|
||||
import android.net.Uri
|
||||
import io.novafoundation.nova.common.utils.DialogMessageManager
|
||||
import io.novafoundation.nova.common.utils.TokenSymbol
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.CallbackEvent
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.runtime.ext.isEnabled
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
|
||||
import io.novafoundation.nova.feature_gift_impl.R
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.ClaimGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.claim.ClaimGiftPayload
|
||||
import io.novafoundation.nova.runtime.ext.ChainGeneses
|
||||
import io.novafoundation.nova.runtime.ext.utilityAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.findChain
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import java.security.InvalidParameterException
|
||||
|
||||
class ClaimGiftDeepLinkHandler(
|
||||
private val router: GiftRouter,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val automaticInteractionGate: AutomaticInteractionGate,
|
||||
private val dialogMessageManager: DialogMessageManager,
|
||||
private val claimGiftDeepLinkConfigurator: ClaimGiftDeepLinkConfigurator,
|
||||
private val claimGiftInteractor: ClaimGiftInteractor
|
||||
) : DeepLinkHandler {
|
||||
|
||||
override val callbackFlow = MutableSharedFlow<CallbackEvent>()
|
||||
|
||||
override suspend fun matches(data: Uri): Boolean {
|
||||
val path = data.path ?: return false
|
||||
|
||||
return path.startsWith(claimGiftDeepLinkConfigurator.deepLinkPrefix)
|
||||
}
|
||||
|
||||
override suspend fun handleDeepLink(uri: Uri) = runCatching {
|
||||
automaticInteractionGate.awaitInteractionAllowed()
|
||||
|
||||
val data = uri.getQueryParameter(claimGiftDeepLinkConfigurator.payloadParam) ?: throw InvalidParameterException()
|
||||
val payloadParams = claimGiftDeepLinkConfigurator.fromPayload(data)
|
||||
val secretBytes = payloadParams.seed.fromHex()
|
||||
val chain = findChain(payloadParams.chainIdPrefix) ?: throw InvalidParameterException()
|
||||
val chainAsset = findAsset(chain, payloadParams.symbol)
|
||||
require(chain.isEnabled)
|
||||
|
||||
val claimableGift = claimGiftInteractor.getClaimableGift(secretBytes, chain.id, chainAsset.id)
|
||||
val isGiftAlreadyClaimed = claimGiftInteractor.isGiftAlreadyClaimed(claimableGift)
|
||||
if (isGiftAlreadyClaimed) {
|
||||
dialogMessageManager.showDialog {
|
||||
setTitle(R.string.claim_gift_already_claimed_title)
|
||||
setMessage(R.string.claim_gift_already_claimed_message)
|
||||
setPositiveButton(R.string.common_got_it, null)
|
||||
}
|
||||
|
||||
return@runCatching
|
||||
}
|
||||
|
||||
router.openClaimGift(ClaimGiftPayload(secretBytes, chain.id, chainAsset.id))
|
||||
}
|
||||
|
||||
private suspend fun findChain(chainIdPrefix: String?): Chain? {
|
||||
if (chainIdPrefix == null) return chainRegistry.getChain(ChainGeneses.POLKADOT_ASSET_HUB)
|
||||
|
||||
return chainRegistry.findChain { chainIdPrefix == claimGiftDeepLinkConfigurator.normaliseChainId(it.id) }
|
||||
}
|
||||
|
||||
private fun findAsset(chain: Chain, tokenSymbol: TokenSymbol?): Chain.Asset {
|
||||
if (tokenSymbol == null) return chain.utilityAsset
|
||||
|
||||
return chain.assets.first { it.symbol.value == tokenSymbol.value }
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.claim.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_gift_impl.presentation.claim.ClaimGiftFragment
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.claim.ClaimGiftPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
ClaimGiftModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface ClaimGiftComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: ClaimGiftPayload
|
||||
): ClaimGiftComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: ClaimGiftFragment)
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.claim.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.presentation.AssetIconProvider
|
||||
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.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.mixin.selectSingleWallet.SelectSingleWalletMixin
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.ClaimGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.claim.ClaimGiftPayload
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.claim.ClaimGiftViewModel
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.UnpackingGiftAnimationFactory
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.claim.ClaimGiftMixinFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.TokenFormatter
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class ClaimGiftModule {
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): ClaimGiftViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(ClaimGiftViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(ClaimGiftViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: GiftRouter,
|
||||
payload: ClaimGiftPayload,
|
||||
claimGiftInteractor: ClaimGiftInteractor,
|
||||
chainRegistry: ChainRegistry,
|
||||
unpackingGiftAnimationFactory: UnpackingGiftAnimationFactory,
|
||||
assetIconProvider: AssetIconProvider,
|
||||
tokenFormatter: TokenFormatter,
|
||||
resourceManager: ResourceManager,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
claimGiftMixinFactory: ClaimGiftMixinFactory,
|
||||
accountInteractor: AccountInteractor,
|
||||
selectSingleWalletMixin: SelectSingleWalletMixin.Factory,
|
||||
): ViewModel {
|
||||
return ClaimGiftViewModel(
|
||||
router = router,
|
||||
payload = payload,
|
||||
claimGiftInteractor = claimGiftInteractor,
|
||||
chainRegistry = chainRegistry,
|
||||
unpackingGiftAnimationFactory = unpackingGiftAnimationFactory,
|
||||
assetIconProvider = assetIconProvider,
|
||||
tokenFormatter = tokenFormatter,
|
||||
resourceManager = resourceManager,
|
||||
walletUiUseCase = walletUiUseCase,
|
||||
claimGiftMixinFactory = claimGiftMixinFactory,
|
||||
selectSingleWalletMixin = selectSingleWalletMixin,
|
||||
accountInteractor = accountInteractor
|
||||
)
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.common
|
||||
|
||||
import androidx.annotation.RawRes
|
||||
import io.novafoundation.nova.common.utils.TokenSymbol
|
||||
import io.novafoundation.nova.feature_gift_impl.R
|
||||
|
||||
private enum class GiftKnownTicker {
|
||||
DOT, KSM, USDT, HDX, AZERO, ASTR, UNKNOW;
|
||||
|
||||
companion object {
|
||||
fun tickerOrUnknown(symbol: TokenSymbol): GiftKnownTicker {
|
||||
return GiftKnownTicker.entries.firstOrNull { it.name == symbol.value } ?: UNKNOW
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface GiftAnimationFactory {
|
||||
|
||||
@RawRes
|
||||
fun getAnimationForAsset(symbol: TokenSymbol): Int
|
||||
}
|
||||
|
||||
class PackingGiftAnimationFactory : GiftAnimationFactory {
|
||||
|
||||
override fun getAnimationForAsset(symbol: TokenSymbol): Int {
|
||||
val ticker = GiftKnownTicker.tickerOrUnknown(symbol)
|
||||
return when (ticker) {
|
||||
GiftKnownTicker.DOT -> R.raw.dot_packing
|
||||
GiftKnownTicker.KSM -> R.raw.ksm_packing
|
||||
GiftKnownTicker.USDT -> R.raw.usdt_packing
|
||||
GiftKnownTicker.HDX -> R.raw.hdx_packing
|
||||
GiftKnownTicker.AZERO -> R.raw.azero_packing
|
||||
GiftKnownTicker.ASTR -> R.raw.astr_packing
|
||||
GiftKnownTicker.UNKNOW -> R.raw.default_packing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UnpackingGiftAnimationFactory : GiftAnimationFactory {
|
||||
|
||||
override fun getAnimationForAsset(symbol: TokenSymbol): Int {
|
||||
val ticker = GiftKnownTicker.tickerOrUnknown(symbol)
|
||||
return when (ticker) {
|
||||
GiftKnownTicker.DOT -> R.raw.dot_upacking
|
||||
GiftKnownTicker.KSM -> R.raw.ksm_unpacking
|
||||
GiftKnownTicker.USDT -> R.raw.usdt_unpacking
|
||||
GiftKnownTicker.HDX -> R.raw.hdx_unpacking
|
||||
GiftKnownTicker.AZERO -> R.raw.azero_unpacking
|
||||
GiftKnownTicker.ASTR -> R.raw.astr_unpacking
|
||||
GiftKnownTicker.UNKNOW -> R.raw.default_unpacking
|
||||
}
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.common
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentCurrency
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.CreateGiftModel
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.GiftFee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransferPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.buildAssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.Asset
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.OriginFee
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.model.amountFromPlanks
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.ext.emptyAccountIdKey
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainWithAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import java.math.BigDecimal
|
||||
|
||||
fun buildGiftValidationPayload(
|
||||
createGiftModel: CreateGiftModel,
|
||||
asset: Asset,
|
||||
transferMax: Boolean,
|
||||
feePaymentCurrency: FeePaymentCurrency,
|
||||
fee: GiftFee,
|
||||
): AssetTransferPayload {
|
||||
val transferAmount = createGiftModel.amount + createGiftModel.chainAsset.amountFromPlanks(fee.claimGiftFee.amount)
|
||||
|
||||
val transfer = buildTransfer(
|
||||
metaAccount = createGiftModel.senderMetaAccount,
|
||||
chain = createGiftModel.chain,
|
||||
chainAsset = createGiftModel.chainAsset,
|
||||
amount = transferAmount,
|
||||
transferringMaxAmount = transferMax,
|
||||
feePaymentCurrency = feePaymentCurrency,
|
||||
address = createGiftModel.chain.addressOf(createGiftModel.chain.emptyAccountIdKey()),
|
||||
)
|
||||
|
||||
val originFee = OriginFee(
|
||||
submissionFee = fee.createGiftFee,
|
||||
deliveryFee = null
|
||||
)
|
||||
|
||||
return AssetTransferPayload(
|
||||
transfer = WeightedAssetTransfer(
|
||||
assetTransfer = transfer,
|
||||
fee = originFee
|
||||
),
|
||||
crossChainFee = null,
|
||||
originFee = originFee,
|
||||
originCommissionAsset = asset,
|
||||
originUsedAsset = asset
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildTransfer(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
feePaymentCurrency: FeePaymentCurrency,
|
||||
amount: BigDecimal,
|
||||
transferringMaxAmount: Boolean,
|
||||
address: String,
|
||||
): AssetTransfer {
|
||||
val chainWithAsset = ChainWithAsset(chain, chainAsset)
|
||||
return buildAssetTransfer(
|
||||
metaAccount = metaAccount,
|
||||
feePaymentCurrency = feePaymentCurrency,
|
||||
origin = chainWithAsset,
|
||||
destination = chainWithAsset,
|
||||
amount = amount,
|
||||
transferringMaxAmount = transferringMaxAmount,
|
||||
address = address
|
||||
)
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.common.claim
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.ClaimableGift
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.GiftAmountWithFee
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
interface ClaimGiftMixin {
|
||||
|
||||
val claimingInProgressFlow: MutableStateFlow<Boolean>
|
||||
|
||||
suspend fun claimGift(
|
||||
gift: ClaimableGift,
|
||||
amountWithFee: GiftAmountWithFee,
|
||||
giftMetaAccount: MetaAccount,
|
||||
giftRecipient: MetaAccount
|
||||
): Result<Unit>
|
||||
}
|
||||
|
||||
sealed class ClaimGiftException : Exception() {
|
||||
class GiftAlreadyClaimed : ClaimGiftException()
|
||||
|
||||
class UnknownError(throwable: Throwable) : ClaimGiftException()
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.common.claim
|
||||
|
||||
import io.novafoundation.nova.common.utils.mapFailure
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.ClaimGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.ClaimableGift
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.GiftAmountWithFee
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class ClaimGiftMixinFactory(private val claimGiftInteractor: ClaimGiftInteractor) {
|
||||
|
||||
fun create(
|
||||
coroutineScope: CoroutineScope
|
||||
): ClaimGiftMixin {
|
||||
return RealClaimGiftMixin(
|
||||
claimGiftInteractor,
|
||||
coroutineScope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class RealClaimGiftMixin(
|
||||
private val claimGiftInteractor: ClaimGiftInteractor,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) : ClaimGiftMixin {
|
||||
|
||||
override val claimingInProgressFlow = MutableStateFlow(false)
|
||||
|
||||
override suspend fun claimGift(
|
||||
gift: ClaimableGift,
|
||||
amountWithFee: GiftAmountWithFee,
|
||||
giftMetaAccount: MetaAccount,
|
||||
giftRecipient: MetaAccount
|
||||
): Result<Unit> {
|
||||
claimingInProgressFlow.value = true
|
||||
|
||||
if (claimGiftInteractor.isGiftAlreadyClaimed(gift)) {
|
||||
claimingInProgressFlow.value = false
|
||||
|
||||
return Result.failure(ClaimGiftException.GiftAlreadyClaimed())
|
||||
}
|
||||
|
||||
return claimGiftInteractor.claimGift(
|
||||
claimableGift = gift,
|
||||
giftAmountWithFee = amountWithFee,
|
||||
giftMetaAccount = giftMetaAccount,
|
||||
giftRecipient = giftRecipient,
|
||||
coroutineScope = coroutineScope
|
||||
)
|
||||
.mapFailure {
|
||||
claimingInProgressFlow.value = false
|
||||
|
||||
ClaimGiftException.UnknownError(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.confirm
|
||||
|
||||
import android.view.View
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.mixin.impl.observeValidations
|
||||
import io.novafoundation.nova.common.presentation.showLoadingState
|
||||
import io.novafoundation.nova.common.utils.FragmentPayloadCreator
|
||||
import io.novafoundation.nova.common.utils.PayloadCreator
|
||||
import io.novafoundation.nova.common.utils.insets.ImeInsetsState
|
||||
import io.novafoundation.nova.common.utils.insets.applySystemBarInsets
|
||||
import io.novafoundation.nova.common.utils.payload
|
||||
import io.novafoundation.nova.common.view.setState
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.setupExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.view.showAddress
|
||||
import io.novafoundation.nova.feature_account_api.view.showChain
|
||||
import io.novafoundation.nova.feature_account_api.view.showWallet
|
||||
import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_impl.databinding.FragmentCreateGiftConfirmBinding
|
||||
import io.novafoundation.nova.feature_gift_impl.di.GiftFeatureComponent
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.view.showAmount
|
||||
|
||||
class CreateGiftConfirmFragment : BaseFragment<CreateGiftConfirmViewModel, FragmentCreateGiftConfirmBinding>() {
|
||||
|
||||
companion object : PayloadCreator<CreateGiftConfirmPayload> by FragmentPayloadCreator()
|
||||
|
||||
override fun createBinding() = FragmentCreateGiftConfirmBinding.inflate(layoutInflater)
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.root.applySystemBarInsets(imeInsets = ImeInsetsState.ENABLE_IF_SUPPORTED)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.confirmCreateGiftToolbar.setHomeButtonListener { viewModel.back() }
|
||||
|
||||
binder.confirmCreateGiftAccount.setOnClickListener { viewModel.accountClicked() }
|
||||
|
||||
binder.confirmCreateGiftButton.prepareForProgress(this)
|
||||
binder.confirmCreateGiftButton.setOnClickListener { viewModel.confirmClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<GiftFeatureComponent>(this, GiftFeatureApi::class.java)
|
||||
.createGiftConfirmComponentFactory()
|
||||
.create(this, payload())
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: CreateGiftConfirmViewModel) {
|
||||
setupExternalActions(viewModel)
|
||||
observeValidations(viewModel)
|
||||
viewModel.feeMixin.setupGiftFeeLoading(binder.confirmCreateGiftNetworkFee, binder.confirmCreateGiftClaimFee)
|
||||
|
||||
viewModel.senderGiftAccount.observe(binder.confirmCreateGiftAccount::showAddress)
|
||||
|
||||
viewModel.confirmButtonStateLiveData.observe(binder.confirmCreateGiftButton::setState)
|
||||
|
||||
viewModel.wallet.observe(binder.confirmCreateGiftWallet::showWallet)
|
||||
|
||||
viewModel.chainModelFlow.observe { binder.confirmCreateGiftNetwork.showChain(it) }
|
||||
|
||||
viewModel.totalAmountModel.observe(binder.confirmCreateGiftTotalAmount::showLoadingState)
|
||||
|
||||
viewModel.giftAmountModel.observe(binder.confirmCreateGiftAmount::showAmount)
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.confirm
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
import java.math.BigDecimal
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class CreateGiftConfirmPayload(
|
||||
val amount: BigDecimal,
|
||||
val transferringMaxAmount: Boolean,
|
||||
val assetPayload: AssetPayload
|
||||
) : Parcelable
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.confirm
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.domain.asLoaded
|
||||
import io.novafoundation.nova.common.mixin.api.Validatable
|
||||
import io.novafoundation.nova.common.presentation.DescriptiveButtonState
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.common.utils.launchUnit
|
||||
import io.novafoundation.nova.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.common.validation.progressConsumer
|
||||
import io.novafoundation.nova.feature_account_api.data.mappers.mapChainToUi
|
||||
import io.novafoundation.nova.feature_account_api.data.model.SubmissionFee
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAddressIn
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.icon.createAddressModel
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.showAddressActions
|
||||
import io.novafoundation.nova.feature_gift_impl.R
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.CreateGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.GiftId
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.CreateGiftModel
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.amount.fee.GiftFeeDisplayFormatter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.amount.fee.createForGiftsWithGiftFeeDisplay
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.buildGiftValidationPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.formatAmountToAmountModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.model.AmountConfig
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.awaitFee
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AmountSign
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.validation.transfers.autoFixSendValidationPayload
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.validation.transfers.mapAssetTransferValidationFailureToUI
|
||||
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.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CreateGiftConfirmViewModel(
|
||||
private val router: GiftRouter,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val validationExecutor: ValidationExecutor,
|
||||
private val assetUseCase: ArbitraryAssetUseCase,
|
||||
private val walletUiUseCase: WalletUiUseCase,
|
||||
private val payload: CreateGiftConfirmPayload,
|
||||
private val amountFormatter: AmountFormatter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val externalActions: ExternalActions.Presentation,
|
||||
private val createGiftInteractor: CreateGiftInteractor,
|
||||
private val addressIconGenerator: AddressIconGenerator,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
feeLoaderMixinFactory: FeeLoaderMixinV2.Factory,
|
||||
) : BaseViewModel(),
|
||||
ExternalActions by externalActions,
|
||||
Validatable by validationExecutor {
|
||||
|
||||
private val chainFlow = chainRegistry.chainFlow(payload.assetPayload.chainId)
|
||||
private val chainAssetFlow = chainFlow.map { it.assetsById.getValue(payload.assetPayload.chainAssetId) }
|
||||
|
||||
private val metaAccountFlow = selectedAccountUseCase.selectedMetaAccountFlow()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val assetFlow = chainAssetFlow.flatMapLatest(assetUseCase::assetFlow)
|
||||
.shareInBackground()
|
||||
|
||||
val chainModelFlow = chainFlow.map {
|
||||
mapChainToUi(it)
|
||||
}.shareInBackground()
|
||||
|
||||
val wallet = walletUiUseCase.selectedWalletUiFlow()
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val senderGiftAccount = combine(metaAccountFlow, chainFlow) { metaAccount, chain ->
|
||||
createAddressModel(
|
||||
address = metaAccount.requireAddressIn(chain),
|
||||
chain = chain
|
||||
)
|
||||
}
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
private val feeFormatter = GiftFeeDisplayFormatter(amountFormatter)
|
||||
val feeMixin = feeLoaderMixinFactory.createForGiftsWithGiftFeeDisplay(
|
||||
chainAssetFlow,
|
||||
feeFormatter
|
||||
)
|
||||
|
||||
val totalAmountModel = assetFlow.map { asset ->
|
||||
amountFormatter.formatAmountToAmountModel(payload.amount, asset, AmountConfig(tokenAmountSign = AmountSign.NEGATIVE))
|
||||
.asLoaded()
|
||||
}
|
||||
|
||||
val giftAmountModel = assetFlow.map { asset ->
|
||||
amountFormatter.formatAmountToAmountModel(payload.amount, asset)
|
||||
}
|
||||
|
||||
private val validationInProgressFlow = MutableStateFlow(false)
|
||||
val confirmButtonStateLiveData = validationInProgressFlow.map { submitting ->
|
||||
if (submitting) {
|
||||
DescriptiveButtonState.Loading
|
||||
} else {
|
||||
DescriptiveButtonState.Enabled(resourceManager.getString(R.string.common_confirm))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
setupFees()
|
||||
}
|
||||
|
||||
fun back() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun accountClicked() = launchUnit {
|
||||
val chain = chainFlow.first()
|
||||
val address = senderGiftAccount.first()
|
||||
externalActions.showAddressActions(address.address, chain)
|
||||
}
|
||||
|
||||
fun confirmClicked() = launchUnit {
|
||||
validationInProgressFlow.value = true
|
||||
val fee = feeMixin.awaitFee()
|
||||
val chain = chainFlow.first()
|
||||
|
||||
val giftModel = CreateGiftModel(
|
||||
senderMetaAccount = selectedAccountUseCase.getSelectedMetaAccount(),
|
||||
chain = chain,
|
||||
chainAsset = chainAssetFlow.first(),
|
||||
amount = payload.amount,
|
||||
)
|
||||
|
||||
val payload = buildGiftValidationPayload(
|
||||
giftModel,
|
||||
asset = assetFlow.first(),
|
||||
payload.transferringMaxAmount,
|
||||
feeMixin.feePaymentCurrency(),
|
||||
fee
|
||||
)
|
||||
|
||||
validationExecutor.requireValid(
|
||||
validationSystem = createGiftInteractor.validationSystemFor(giftModel.chainAsset, viewModelScope),
|
||||
payload = payload,
|
||||
progressConsumer = validationInProgressFlow.progressConsumer(),
|
||||
autoFixPayload = ::autoFixSendValidationPayload,
|
||||
validationFailureTransformerCustom = { status, actions ->
|
||||
viewModelScope.mapAssetTransferValidationFailureToUI(
|
||||
resourceManager = resourceManager,
|
||||
status = status,
|
||||
actions = actions,
|
||||
setFee = { feeMixin.setFee(fee.replaceSubmission(it)) }
|
||||
)
|
||||
},
|
||||
) { validPayload ->
|
||||
performTransfer(giftModel, validPayload.transfer, validPayload.originFee.submissionFee)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performTransfer(
|
||||
giftModel: CreateGiftModel,
|
||||
transfer: WeightedAssetTransfer,
|
||||
fee: SubmissionFee
|
||||
) = launch {
|
||||
createGiftInteractor.createAndSaveGift(giftModel, transfer, fee, viewModelScope)
|
||||
.onSuccess {
|
||||
finishCreateGift(giftId = it)
|
||||
}.onFailure(::showError)
|
||||
|
||||
validationInProgressFlow.value = false
|
||||
}
|
||||
|
||||
private fun finishCreateGift(giftId: GiftId) {
|
||||
router.openGiftSharing(giftId)
|
||||
}
|
||||
|
||||
private fun setupFees() {
|
||||
feeMixin.loadFee {
|
||||
val metaAccount = metaAccountFlow.first()
|
||||
val chain = chainFlow.first()
|
||||
|
||||
val createGiftModel = CreateGiftModel(
|
||||
senderMetaAccount = metaAccount,
|
||||
chain = chain,
|
||||
chainAsset = chainAssetFlow.first(),
|
||||
amount = payload.amount,
|
||||
)
|
||||
|
||||
createGiftInteractor.getFee(
|
||||
createGiftModel,
|
||||
payload.transferringMaxAmount,
|
||||
viewModelScope
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createAddressModel(
|
||||
address: String,
|
||||
chain: Chain
|
||||
) = addressIconGenerator.createAddressModel(
|
||||
chain = chain,
|
||||
sizeInDp = AddressIconGenerator.SIZE_MEDIUM,
|
||||
address = address,
|
||||
background = AddressIconGenerator.BACKGROUND_TRANSPARENT,
|
||||
addressDisplayUseCase = null
|
||||
)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.confirm.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_gift_impl.presentation.confirm.CreateGiftConfirmFragment
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.confirm.CreateGiftConfirmPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
CreateGiftConfirmModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface CreateGiftConfirmComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: CreateGiftConfirmPayload
|
||||
): CreateGiftConfirmComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: CreateGiftConfirmFragment)
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.confirm.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.common.validation.ValidationExecutor
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.actions.ExternalActions
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.CreateGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.confirm.CreateGiftConfirmPayload
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.confirm.CreateGiftConfirmViewModel
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.ArbitraryAssetUseCase
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class CreateGiftConfirmModule {
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): CreateGiftConfirmViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(CreateGiftConfirmViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(CreateGiftConfirmViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: GiftRouter,
|
||||
chainRegistry: ChainRegistry,
|
||||
validationExecutor: ValidationExecutor,
|
||||
assetUseCase: ArbitraryAssetUseCase,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
payload: CreateGiftConfirmPayload,
|
||||
amountFormatter: AmountFormatter,
|
||||
resourceManager: ResourceManager,
|
||||
externalActions: ExternalActions.Presentation,
|
||||
createGiftInteractor: CreateGiftInteractor,
|
||||
addressIconGenerator: AddressIconGenerator,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
feeLoaderMixinFactory: FeeLoaderMixinV2.Factory,
|
||||
): ViewModel {
|
||||
return CreateGiftConfirmViewModel(
|
||||
router = router,
|
||||
chainRegistry = chainRegistry,
|
||||
validationExecutor = validationExecutor,
|
||||
assetUseCase = assetUseCase,
|
||||
walletUiUseCase = walletUiUseCase,
|
||||
payload = payload,
|
||||
amountFormatter = amountFormatter,
|
||||
resourceManager = resourceManager,
|
||||
externalActions = externalActions,
|
||||
createGiftInteractor = createGiftInteractor,
|
||||
addressIconGenerator = addressIconGenerator,
|
||||
selectedAccountUseCase = selectedAccountUseCase,
|
||||
feeLoaderMixinFactory = feeLoaderMixinFactory
|
||||
)
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.confirm
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.GiftFee
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.amount.fee.GiftFeeDisplay
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.model.mapDisplay
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.FeeLoaderMixinV2
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.v2.setupFeeLoading
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView
|
||||
|
||||
context(BaseFragment<V, *>)
|
||||
fun <V : BaseViewModel> FeeLoaderMixinV2<GiftFee, GiftFeeDisplay>.setupGiftFeeLoading(networkFee: FeeView, claimFee: FeeView) {
|
||||
setupFeeLoading(
|
||||
setFeeStatus = {
|
||||
val originFee = it.mapDisplay(GiftFeeDisplay::networkFee)
|
||||
val crossChainFee = it.mapDisplay(GiftFeeDisplay::claimGiftFee)
|
||||
|
||||
networkFee.setFeeStatus(originFee)
|
||||
claimFee.setFeeStatus(crossChainFee)
|
||||
},
|
||||
setUserCanChangeFeeAsset = {
|
||||
networkFee.setFeeEditable(it) {
|
||||
changePaymentCurrencyClicked()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.gifts
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import io.novafoundation.nova.common.utils.images.Icon
|
||||
|
||||
data class GiftRVItem(
|
||||
val id: Long,
|
||||
val isClaimed: Boolean,
|
||||
val amount: CharSequence,
|
||||
val assetIcon: Icon,
|
||||
val subtitle: String,
|
||||
@DrawableRes val imageRes: Int
|
||||
)
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.gifts
|
||||
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.list.CustomPlaceholderAdapter
|
||||
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_gift_api.di.GiftFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_impl.R
|
||||
import io.novafoundation.nova.feature_gift_impl.databinding.FragmentGiftsBinding
|
||||
import io.novafoundation.nova.feature_gift_impl.di.GiftFeatureComponent
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.gifts.list.GiftsHeaderAdapter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.gifts.list.GiftsInstructionsAdapter
|
||||
import javax.inject.Inject
|
||||
|
||||
class GiftsFragment : BaseFragment<GiftsViewModel, FragmentGiftsBinding>(), GiftsHeaderAdapter.ItemHandler, GiftsListAdapter.Handler {
|
||||
|
||||
companion object : PayloadCreator<GiftsPayload> by FragmentPayloadCreator()
|
||||
|
||||
override fun createBinding() = FragmentGiftsBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val headerAdapter = GiftsHeaderAdapter(this)
|
||||
|
||||
private val instructionsAdapter = GiftsInstructionsAdapter()
|
||||
|
||||
private val giftsTitleAdapter = CustomPlaceholderAdapter(R.layout.item_gifts_title)
|
||||
|
||||
private val giftsAdapter by lazy(LazyThreadSafetyMode.NONE) { GiftsListAdapter(this, imageLoader) }
|
||||
|
||||
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ConcatAdapter(
|
||||
headerAdapter,
|
||||
giftsTitleAdapter,
|
||||
giftsAdapter,
|
||||
instructionsAdapter
|
||||
)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.giftsToolbar.setHomeButtonListener { viewModel.back() }
|
||||
binder.giftsList.adapter = adapter
|
||||
|
||||
binder.giftsCreate.setOnClickListener { viewModel.createGiftClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<GiftFeatureComponent>(this, GiftFeatureApi::class.java)
|
||||
.giftsComponentFactory()
|
||||
.create(this, payload())
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: GiftsViewModel) {
|
||||
observeBrowserEvents(viewModel)
|
||||
|
||||
viewModel.gifts.observe {
|
||||
instructionsAdapter.show(it.isEmpty())
|
||||
giftsTitleAdapter.show(it.isNotEmpty())
|
||||
giftsAdapter.submitList(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLearnMoreClicked() {
|
||||
viewModel.learnMoreClicked()
|
||||
}
|
||||
|
||||
override fun onGiftClicked(referendum: GiftRVItem) {
|
||||
viewModel.giftClicked(referendum)
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.gifts
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.list.BaseViewHolder
|
||||
import io.novafoundation.nova.common.utils.AlphaColorFilter
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.common.view.shape.getRoundedCornerDrawableWithRipple
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon
|
||||
import io.novafoundation.nova.feature_gift_impl.R
|
||||
import io.novafoundation.nova.feature_gift_impl.databinding.ItemGiftBinding
|
||||
|
||||
class GiftsListAdapter(
|
||||
private val handler: Handler,
|
||||
private val imageLoader: ImageLoader
|
||||
) : ListAdapter<GiftRVItem, GiftViewHolder>(GiftsDiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): GiftViewHolder {
|
||||
return GiftViewHolder(
|
||||
ItemGiftBinding.inflate(parent.inflater(), parent, false),
|
||||
handler,
|
||||
imageLoader
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: GiftViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
interface Handler {
|
||||
|
||||
fun onGiftClicked(referendum: GiftRVItem)
|
||||
}
|
||||
}
|
||||
|
||||
private object GiftsDiffCallback : DiffUtil.ItemCallback<GiftRVItem>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: GiftRVItem,
|
||||
newItem: GiftRVItem
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: GiftRVItem,
|
||||
newItem: GiftRVItem
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
class GiftViewHolder(
|
||||
private val binder: ItemGiftBinding,
|
||||
private val handler: GiftsListAdapter.Handler,
|
||||
private val imageLoader: ImageLoader
|
||||
) : BaseViewHolder(binder.root) {
|
||||
|
||||
fun bind(item: GiftRVItem) = with(binder) {
|
||||
// Set content
|
||||
giftAmount.text = item.amount
|
||||
giftAssetIcon.setTokenIcon(item.assetIcon, imageLoader)
|
||||
giftCreationDate.text = item.subtitle
|
||||
giftImage.setImageResource(item.imageRes)
|
||||
|
||||
root.setOnClickListener { handler.onGiftClicked(item) }
|
||||
|
||||
val amountColor = if (item.isClaimed) R.color.text_secondary else R.color.text_primary
|
||||
val assetIconAlpha = if (item.isClaimed) 0.56f else 1f
|
||||
|
||||
giftAmount.setTextColorRes(amountColor)
|
||||
giftAssetIcon.colorFilter = AlphaColorFilter(assetIconAlpha)
|
||||
|
||||
giftAmountChevron.isVisible = !item.isClaimed
|
||||
root.background = if (item.isClaimed) background(R.color.block_background) else background(R.color.gift_block_background)
|
||||
}
|
||||
|
||||
private fun background(@ColorRes colorRes: Int): Drawable {
|
||||
return context.getRoundedCornerDrawableWithRipple(fillColorRes = colorRes, cornerSizeInDp = 12)
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.gifts
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.model.AssetPayload
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
sealed interface GiftsPayload : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
object AllAssets : GiftsPayload
|
||||
|
||||
@Parcelize
|
||||
data class ByAsset(val assetPayload: AssetPayload) : GiftsPayload
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.gifts
|
||||
|
||||
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.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
|
||||
import io.novafoundation.nova.feature_gift_impl.R
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.GiftsInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.Gift
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.models.isClaimed
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
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.feature_wallet_api.presentation.model.fullChainAssetId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.ChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.FullChainAssetId
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class GiftsViewModel(
|
||||
private val router: GiftRouter,
|
||||
private val appLinksProvider: AppLinksProvider,
|
||||
private val giftsInteractor: GiftsInteractor,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val giftsPayload: GiftsPayload,
|
||||
private val tokenFormatter: TokenFormatter,
|
||||
private val assetIconProvider: AssetIconProvider,
|
||||
private val resourceManager: ResourceManager
|
||||
) : BaseViewModel(), Browserable {
|
||||
|
||||
private val dateFormatter: SimpleDateFormat = SimpleDateFormat("d.M.yyyy", Locale.getDefault())
|
||||
|
||||
override val openBrowserEvent = MutableLiveData<Event<String>>()
|
||||
|
||||
private val chains = chainRegistry.chainsById
|
||||
|
||||
val gifts = combine(
|
||||
giftsInteractor.observeGifts(giftsPayload.fullChainAssetIdOrNull()),
|
||||
chains
|
||||
) { gifts, chains ->
|
||||
gifts.mapNotNull { mapGift(it, chains) }
|
||||
}
|
||||
|
||||
init {
|
||||
launch {
|
||||
giftsInteractor.syncGiftsState()
|
||||
}
|
||||
}
|
||||
|
||||
fun back() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun learnMoreClicked() {
|
||||
openBrowserEvent.value = Event(appLinksProvider.giftsWikiUrl)
|
||||
}
|
||||
|
||||
fun createGiftClicked() {
|
||||
when (giftsPayload) {
|
||||
GiftsPayload.AllAssets -> router.openGiftsFlow()
|
||||
is GiftsPayload.ByAsset -> router.openSelectGiftAmount(giftsPayload.assetPayload)
|
||||
}
|
||||
}
|
||||
|
||||
fun giftClicked(gift: GiftRVItem) {
|
||||
if (gift.isClaimed) return
|
||||
|
||||
router.openGiftSharing(gift.id, isSecondOpen = true)
|
||||
}
|
||||
|
||||
private fun mapGift(gift: Gift, chains: Map<ChainId, Chain>): GiftRVItem? {
|
||||
val asset = chains[gift.chainId]?.assetsById[gift.assetId] ?: return null
|
||||
val isClaimed = gift.status.isClaimed()
|
||||
val subtitle = when (gift.status) {
|
||||
Gift.Status.CLAIMED -> resourceManager.getString(R.string.gift_claimed_subtitle)
|
||||
Gift.Status.RECLAIMED -> resourceManager.getString(R.string.gift_reclaimed_subtitle)
|
||||
Gift.Status.PENDING -> resourceManager.getString(
|
||||
R.string.gift_created_subtitle,
|
||||
dateFormatter.format(gift.creationDate)
|
||||
)
|
||||
}
|
||||
|
||||
return GiftRVItem(
|
||||
id = gift.id,
|
||||
isClaimed = isClaimed,
|
||||
amount = tokenFormatter.formatToken(gift.amount, asset),
|
||||
assetIcon = assetIconProvider.getAssetIconOrFallback(asset),
|
||||
subtitle = subtitle,
|
||||
imageRes = if (isClaimed) R.drawable.ic_gift_unpacked else R.drawable.ic_gift_packed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun GiftsPayload.fullChainAssetIdOrNull(): FullChainAssetId? {
|
||||
return when (this) {
|
||||
is GiftsPayload.ByAsset -> assetPayload.fullChainAssetId
|
||||
GiftsPayload.AllAssets -> null
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.gifts.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_gift_impl.presentation.gifts.GiftsFragment
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.gifts.GiftsPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
GiftsModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface GiftsComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: GiftsPayload
|
||||
): GiftsComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: GiftsFragment)
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.gifts.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.data.network.AppLinksProvider
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.GiftsInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.gifts.GiftsPayload
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.gifts.GiftsViewModel
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.TokenFormatter
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class GiftsModule {
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): GiftsViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(GiftsViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(GiftsViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: GiftRouter,
|
||||
appLinksProvider: AppLinksProvider,
|
||||
giftsInteractor: GiftsInteractor,
|
||||
giftsPayload: GiftsPayload,
|
||||
chainRegistry: ChainRegistry,
|
||||
tokenFormatter: TokenFormatter,
|
||||
assetIconProvider: AssetIconProvider,
|
||||
resourceManager: ResourceManager
|
||||
): ViewModel {
|
||||
return GiftsViewModel(
|
||||
router = router,
|
||||
appLinksProvider = appLinksProvider,
|
||||
giftsInteractor = giftsInteractor,
|
||||
giftsPayload = giftsPayload,
|
||||
tokenFormatter = tokenFormatter,
|
||||
assetIconProvider = assetIconProvider,
|
||||
chainRegistry = chainRegistry,
|
||||
resourceManager = resourceManager
|
||||
)
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.gifts.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.novafoundation.nova.common.list.SingleItemAdapter
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.feature_gift_impl.databinding.ItemGiftsHeaderBinding
|
||||
|
||||
class GiftsHeaderAdapter(
|
||||
private val handler: ItemHandler
|
||||
) : SingleItemAdapter<GiftsHeaderHolder>(isShownByDefault = true) {
|
||||
|
||||
interface ItemHandler {
|
||||
fun onLearnMoreClicked()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GiftsHeaderHolder {
|
||||
return GiftsHeaderHolder(
|
||||
ItemGiftsHeaderBinding.inflate(parent.inflater(), parent, false),
|
||||
handler
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: GiftsHeaderHolder, position: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
class GiftsHeaderHolder(binder: ItemGiftsHeaderBinding, handler: GiftsHeaderAdapter.ItemHandler) : RecyclerView.ViewHolder(binder.root) {
|
||||
|
||||
init {
|
||||
binder.giftsHeaderLearnMore.setOnClickListener { handler.onLearnMoreClicked() }
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.gifts.list
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.novafoundation.nova.common.list.SingleItemAdapter
|
||||
import io.novafoundation.nova.common.utils.addColor
|
||||
import io.novafoundation.nova.common.utils.formatting.spannable.spannableFormatting
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.feature_gift_impl.R
|
||||
import io.novafoundation.nova.feature_gift_impl.databinding.ItemGiftsInstructionPlaceholderBinding
|
||||
|
||||
class GiftsInstructionsAdapter() : SingleItemAdapter<GiftsInstructionsHolder>(isShownByDefault = false) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GiftsInstructionsHolder {
|
||||
return GiftsInstructionsHolder(ItemGiftsInstructionPlaceholderBinding.inflate(parent.inflater(), parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: GiftsInstructionsHolder, position: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
class GiftsInstructionsHolder(binder: ItemGiftsInstructionPlaceholderBinding) : RecyclerView.ViewHolder(binder.root) {
|
||||
|
||||
init {
|
||||
with(binder.root.context) {
|
||||
val highlightColor = getColor(R.color.text_primary)
|
||||
binder.giftsInstructionStep1.setStepText(getString(R.string.gifts_placeholder_step_1).addColor(highlightColor))
|
||||
binder.giftsInstructionStep2.setStepText(
|
||||
getString(R.string.gifts_placeholder_step_2)
|
||||
.spannableFormatting(getString(R.string.gifts_placeholder_step_2_highlight).addColor(highlightColor))
|
||||
)
|
||||
binder.giftsInstructionStep3.setStepText(
|
||||
getString(R.string.gifts_placeholder_step_3)
|
||||
.spannableFormatting(getString(R.string.gifts_placeholder_step_3_highlight).addColor(highlightColor))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.share
|
||||
|
||||
import androidx.annotation.RawRes
|
||||
|
||||
class ShareGiftAnimationState(
|
||||
@RawRes val res: Int,
|
||||
val state: State
|
||||
) {
|
||||
enum class State {
|
||||
START,
|
||||
IDLE_END
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.share
|
||||
|
||||
import android.animation.Animator
|
||||
import android.view.View
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.R
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
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.common.utils.share.shareImageWithText
|
||||
import io.novafoundation.nova.common.view.dialog.warningDialog
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.setTokenIcon
|
||||
import io.novafoundation.nova.feature_gift_api.di.GiftFeatureApi
|
||||
import io.novafoundation.nova.feature_gift_impl.databinding.FragmentShareGiftBinding
|
||||
import io.novafoundation.nova.feature_gift_impl.di.GiftFeatureComponent
|
||||
import javax.inject.Inject
|
||||
|
||||
class ShareGiftFragment : BaseFragment<ShareGiftViewModel, FragmentShareGiftBinding>() {
|
||||
|
||||
companion object : PayloadCreator<ShareGiftPayload> by FragmentPayloadCreator()
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val giftAnimationListener = object : Animator.AnimatorListener {
|
||||
override fun onAnimationCancel(animation: Animator) {}
|
||||
override fun onAnimationRepeat(animation: Animator) {}
|
||||
override fun onAnimationStart(animation: Animator) {}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
showAllViews()
|
||||
}
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentShareGiftBinding.inflate(layoutInflater)
|
||||
|
||||
override fun initViews() {
|
||||
binder.shareGiftToolbar.setHomeButtonListener { viewModel.back() }
|
||||
binder.shareGiftToolbar.setRightActionClickListener { viewModel.reclaimClicked() }
|
||||
|
||||
binder.shareGiftButton.setOnClickListener { viewModel.shareDeepLinkClicked() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<GiftFeatureComponent>(this, GiftFeatureApi::class.java)
|
||||
.shareGiftComponentFactory()
|
||||
.create(this, payload())
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: ShareGiftViewModel) {
|
||||
viewModel.giftAnimationRes.observe {
|
||||
binder.shareGiftAnimation.setAnimation(it.res)
|
||||
when (it.state) {
|
||||
ShareGiftAnimationState.State.START -> {
|
||||
binder.shareGiftAnimation.playAnimation()
|
||||
binder.shareGiftAnimation.addAnimatorListener(giftAnimationListener)
|
||||
}
|
||||
|
||||
ShareGiftAnimationState.State.IDLE_END -> {
|
||||
binder.shareGiftAnimation.progress = 1f
|
||||
showAllViews(withAnimation = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.amountModel.observe {
|
||||
binder.shareGiftTokenIcon.setTokenIcon(it.tokenIcon, imageLoader)
|
||||
binder.shareGiftAmount.text = it.amount
|
||||
}
|
||||
|
||||
viewModel.shareEvent.observeEvent {
|
||||
shareImageWithText(sharingData = it, chooserTitle = null)
|
||||
}
|
||||
|
||||
viewModel.isReclaimInProgress.observe {
|
||||
binder.shareGiftToolbar.showProgress(it)
|
||||
}
|
||||
|
||||
viewModel.reclaimButtonVisible.observe {
|
||||
binder.shareGiftToolbar.setRightTextVisible(it)
|
||||
}
|
||||
|
||||
viewModel.confirmReclaimGiftAction.awaitableActionLiveData.observeEvent { event ->
|
||||
warningDialog(
|
||||
context = providedContext,
|
||||
onPositiveClick = { event.onSuccess(Unit) },
|
||||
positiveTextRes = R.string.common_continue,
|
||||
negativeTextRes = R.string.common_cancel,
|
||||
onNegativeClick = { event.onCancel() },
|
||||
styleRes = R.style.AccentAlertDialogTheme
|
||||
) {
|
||||
setTitle(getString(R.string.reclaim_gift_confirmation_title, event.payload.amount))
|
||||
|
||||
setMessage(R.string.reclaim_gift_confirmation_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAllViews(withAnimation: Boolean = true) {
|
||||
binder.shareGiftToolbar.show(withAnimation)
|
||||
binder.shareGiftTokenIcon.show(withAnimation)
|
||||
binder.shareGiftAmount.show(withAnimation)
|
||||
binder.shareGiftButton.show(withAnimation)
|
||||
binder.shareGiftTitle.show(withAnimation)
|
||||
}
|
||||
|
||||
private fun View.show(withAnimation: Boolean) {
|
||||
if (withAnimation) {
|
||||
animate().alpha(1f)
|
||||
.setDuration(400)
|
||||
.start()
|
||||
} else {
|
||||
alpha = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.share
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class ShareGiftPayload(val giftId: Long, val isSecondOpen: Boolean) : Parcelable
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.share
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.interfaces.FileProvider
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
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.launchUnit
|
||||
import io.novafoundation.nova.common.utils.share.ImageWithTextSharing
|
||||
import io.novafoundation.nova.common.utils.write
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.chain.getAssetIconOrFallback
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.configuring.DeepLinkConfigurator
|
||||
import io.novafoundation.nova.feature_gift_impl.R
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.ClaimGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.ShareGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.PackingGiftAnimationFactory
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.claim.deeplink.ClaimGiftDeepLinkConfigurator
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.claim.deeplink.ClaimGiftDeepLinkData
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.claim.ClaimGiftException
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.claim.ClaimGiftMixinFactory
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.share.model.GiftAmountModel
|
||||
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.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.asset
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val GIFT_FILE_NAME = "share-gift.png"
|
||||
|
||||
class ShareGiftViewModel(
|
||||
private val router: GiftRouter,
|
||||
private val payload: ShareGiftPayload,
|
||||
private val shareGiftInteractor: ShareGiftInteractor,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val packingGiftAnimationFactory: PackingGiftAnimationFactory,
|
||||
private val assetIconProvider: AssetIconProvider,
|
||||
private val tokenFormatter: TokenFormatter,
|
||||
private val claimGiftDeepLinkConfigurator: ClaimGiftDeepLinkConfigurator,
|
||||
private val fileProvider: FileProvider,
|
||||
private val claimGiftMixinFactory: ClaimGiftMixinFactory,
|
||||
private val claimGiftInteractor: ClaimGiftInteractor,
|
||||
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
private val resourceManager: ResourceManager
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val giftFlow = shareGiftInteractor.observeGift(payload.giftId)
|
||||
.shareInBackground()
|
||||
|
||||
private val chainAssetFlow = giftFlow.map {
|
||||
chainRegistry.asset(it.chainId, it.assetId)
|
||||
}.shareInBackground()
|
||||
|
||||
val giftAnimationRes = giftFlow.map {
|
||||
val chainAsset = chainRegistry.asset(it.chainId, it.assetId)
|
||||
val animationRes = packingGiftAnimationFactory.getAnimationForAsset(chainAsset.symbol)
|
||||
|
||||
when (payload.isSecondOpen) {
|
||||
true -> ShareGiftAnimationState(animationRes, ShareGiftAnimationState.State.IDLE_END)
|
||||
false -> ShareGiftAnimationState(animationRes, ShareGiftAnimationState.State.START)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
.shareInBackground()
|
||||
|
||||
val amountModel = giftFlow.map {
|
||||
val chainAsset = chainRegistry.asset(it.chainId, it.assetId)
|
||||
val tokenIcon = assetIconProvider.getAssetIconOrFallback(chainAsset)
|
||||
val giftAmount = tokenFormatter.formatToken(it.amount, chainAsset)
|
||||
GiftAmountModel(tokenIcon, giftAmount)
|
||||
}
|
||||
|
||||
private val _shareEvent = MutableLiveData<Event<ImageWithTextSharing>>()
|
||||
val shareEvent: LiveData<Event<ImageWithTextSharing>> = _shareEvent
|
||||
|
||||
private val claimGiftMixin = claimGiftMixinFactory.create(this)
|
||||
|
||||
private val _isReclaimInProgress = MutableStateFlow(false)
|
||||
val isReclaimInProgress: Flow<Boolean> = _isReclaimInProgress
|
||||
|
||||
val reclaimButtonVisible = flowOf { payload.isSecondOpen }
|
||||
|
||||
val confirmReclaimGiftAction = actionAwaitableMixinFactory.confirmingAction<GiftAmountModel>()
|
||||
|
||||
fun back() {
|
||||
router.finishCreateGift()
|
||||
}
|
||||
|
||||
fun shareDeepLinkClicked() = launchUnit {
|
||||
runCatching {
|
||||
val giftAmount = amountModel.first().amount
|
||||
val sharingLink = getSharingLink()
|
||||
val sharingText = resourceManager.getString(R.string.share_gift_text, giftAmount, sharingLink.toString())
|
||||
val giftImageFile = generateShareImageFile()
|
||||
_shareEvent.value = Event(ImageWithTextSharing(giftImageFile, sharingText))
|
||||
}.onFailure(::showError)
|
||||
}
|
||||
|
||||
private suspend fun getSharingLink(): Uri {
|
||||
val chainAsset = chainAssetFlow.first()
|
||||
val giftSeed = shareGiftInteractor.getGiftSeed(payload.giftId)
|
||||
val sharingPayload = ClaimGiftDeepLinkData(giftSeed, chainAsset.chainId, chainAsset.symbol)
|
||||
return claimGiftDeepLinkConfigurator.configure(sharingPayload, type = DeepLinkConfigurator.Type.APP_LINK)
|
||||
}
|
||||
|
||||
suspend fun generateShareImageFile(): Uri = withContext(Dispatchers.IO) {
|
||||
val shareBitmap = resourceManager.getDrawable(R.drawable.ic_pezkuwi_gift).toBitmap()
|
||||
val file = fileProvider.generateTempFile(fixedName = GIFT_FILE_NAME)
|
||||
file.write(shareBitmap)
|
||||
|
||||
fileProvider.uriOf(file)
|
||||
}
|
||||
|
||||
fun reclaimClicked() = launchUnit {
|
||||
val giftAmount = amountModel.first()
|
||||
confirmReclaimGiftAction.awaitAction(giftAmount)
|
||||
|
||||
_isReclaimInProgress.value = true
|
||||
val giftModel = giftFlow.first()
|
||||
val giftSeed = shareGiftInteractor.getGiftSeed(payload.giftId)
|
||||
val claimableGift = claimGiftInteractor.getClaimableGift(giftSeed.fromHex(), giftModel.chainId, giftModel.assetId)
|
||||
val tempMetaAccount = claimGiftInteractor.createTempMetaAccount(claimableGift)
|
||||
val amountWithFee = claimGiftInteractor.getGiftAmountWithFee(claimableGift, tempMetaAccount, coroutineScope)
|
||||
val recipientMetaAccount = selectedAccountUseCase.getSelectedMetaAccount()
|
||||
|
||||
claimGiftMixin.claimGift(gift = claimableGift, amountWithFee = amountWithFee, giftMetaAccount = tempMetaAccount, giftRecipient = recipientMetaAccount)
|
||||
.onSuccess {
|
||||
shareGiftInteractor.setGiftStateAsReclaimed(giftModel.id)
|
||||
showToast(resourceManager.getString(R.string.reclaim_gift_success))
|
||||
router.back()
|
||||
}
|
||||
.onFailure {
|
||||
_isReclaimInProgress.value = false
|
||||
when (it as ClaimGiftException) {
|
||||
is ClaimGiftException.GiftAlreadyClaimed -> showError(
|
||||
resourceManager.getString(R.string.claim_gift_already_claimed_title),
|
||||
resourceManager.getString(R.string.claim_gift_already_claimed_message)
|
||||
)
|
||||
|
||||
is ClaimGiftException.UnknownError -> showError(
|
||||
resourceManager.getString(R.string.claim_gift_default_error_title),
|
||||
resourceManager.getString(R.string.claim_gift_default_error_message)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.share.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_gift_impl.presentation.share.ShareGiftFragment
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.share.ShareGiftPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
ShareGiftModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface ShareGiftComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: ShareGiftPayload
|
||||
): ShareGiftComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: ShareGiftFragment)
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.share.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.interfaces.FileProvider
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.presentation.AssetIconProvider
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.ClaimGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.domain.ShareGiftInteractor
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.GiftRouter
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.PackingGiftAnimationFactory
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.share.ShareGiftPayload
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.share.ShareGiftViewModel
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.claim.deeplink.ClaimGiftDeepLinkConfigurator
|
||||
import io.novafoundation.nova.feature_gift_impl.presentation.common.claim.ClaimGiftMixinFactory
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.TokenFormatter
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class ShareGiftModule {
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): ShareGiftViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(ShareGiftViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(ShareGiftViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: GiftRouter,
|
||||
payload: ShareGiftPayload,
|
||||
shareGiftInteractor: ShareGiftInteractor,
|
||||
chainRegistry: ChainRegistry,
|
||||
packingGiftAnimationFactory: PackingGiftAnimationFactory,
|
||||
assetIconProvider: AssetIconProvider,
|
||||
tokenFormatter: TokenFormatter,
|
||||
claimGiftDeepLinkConfigurator: ClaimGiftDeepLinkConfigurator,
|
||||
fileProvider: FileProvider,
|
||||
claimGiftMixinFactory: ClaimGiftMixinFactory,
|
||||
claimGiftInteractor: ClaimGiftInteractor,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
resourceManager: ResourceManager
|
||||
): ViewModel {
|
||||
return ShareGiftViewModel(
|
||||
router = router,
|
||||
payload = payload,
|
||||
shareGiftInteractor = shareGiftInteractor,
|
||||
chainRegistry = chainRegistry,
|
||||
packingGiftAnimationFactory = packingGiftAnimationFactory,
|
||||
assetIconProvider = assetIconProvider,
|
||||
tokenFormatter = tokenFormatter,
|
||||
claimGiftDeepLinkConfigurator = claimGiftDeepLinkConfigurator,
|
||||
fileProvider = fileProvider,
|
||||
claimGiftMixinFactory = claimGiftMixinFactory,
|
||||
claimGiftInteractor = claimGiftInteractor,
|
||||
actionAwaitableMixinFactory = actionAwaitableMixinFactory,
|
||||
selectedAccountUseCase = selectedAccountUseCase,
|
||||
resourceManager = resourceManager
|
||||
)
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_gift_impl.presentation.share.model
|
||||
|
||||
import io.novafoundation.nova.common.utils.images.Icon
|
||||
|
||||
class GiftAmountModel(
|
||||
val tokenIcon: Icon,
|
||||
val amount: CharSequence
|
||||
)
|
||||
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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/claimGiftToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
app:homeButtonIcon="@drawable/ic_close"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/claimGiftTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Bold.Title1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:text="@string/claim_gift_title"
|
||||
android:textColor="@color/text_primary"
|
||||
app:layout_constraintTop_toBottomOf="@+id/claimGiftToolbar" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/claimGiftAnimation"
|
||||
android:layout_width="280dp"
|
||||
android:layout_height="280dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/claimGiftAmount"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/claimGiftTitle"
|
||||
app:layout_constraintVertical_bias="0.31"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:lottie_rawRes="@raw/dot_unpacking" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/claimGiftTokenIcon"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/claimGiftAmount"
|
||||
app:layout_constraintEnd_toStartOf="@+id/claimGiftAmount"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="@+id/claimGiftAnimation"
|
||||
app:layout_constraintTop_toTopOf="@+id/claimGiftAmount" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/claimGiftAmount"
|
||||
style="@style/TextAppearance.NovaFoundation.Bold.LargeTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/text_primary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/claimGiftAccount"
|
||||
app:layout_constraintEnd_toEndOf="@+id/claimGiftAnimation"
|
||||
app:layout_constraintStart_toEndOf="@+id/claimGiftTokenIcon"
|
||||
app:layout_constraintTop_toBottomOf="@+id/claimGiftAnimation"
|
||||
tools:text="2 DOT" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/claimGiftAccountTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/text_secondary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/claimGiftAccount"
|
||||
app:layout_constraintStart_toStartOf="@+id/claimGiftAccount"
|
||||
tools:text="Your wallet to receive the gift" />
|
||||
|
||||
<io.novafoundation.nova.feature_account_api.view.AccountView
|
||||
android:id="@+id/claimGiftAccount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/claimGiftAlert"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_goneMarginBottom="24dp" />
|
||||
|
||||
<io.novafoundation.nova.common.view.AlertView
|
||||
android:id="@+id/claimGiftAlert"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/claimGiftButton" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButtonV2
|
||||
android:id="@+id/claimGiftButton"
|
||||
style="@style/Widget.Nova.MaterialButton.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/claim_gift_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,102 @@
|
||||
<?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/confirmCreateGiftToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
app:titleText="@string/common_gift" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<io.novafoundation.nova.feature_wallet_api.presentation.view.amount.PrimaryAmountView
|
||||
android:id="@+id/confirmCreateGiftTotalAmount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp">
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/confirmCreateGiftNetwork"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/common_network" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/confirmCreateGiftWallet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/tabbar_wallet_title" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/confirmCreateGiftAccount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:primaryValueEndIcon="@drawable/ic_info"
|
||||
app:title="@string/common_account" />
|
||||
|
||||
<io.novafoundation.nova.common.view.TableCellView
|
||||
android:id="@+id/confirmCreateGiftAmount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/common_your_gift" />
|
||||
|
||||
<io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView
|
||||
android:id="@+id/confirmCreateGiftNetworkFee"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView
|
||||
android:id="@+id/confirmCreateGiftClaimFee"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/gifts_claim_fee"/>
|
||||
|
||||
</io.novafoundation.nova.common.view.TableView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:gravity="center"
|
||||
android:text="@string/confirm_create_gift_hint" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButtonV2
|
||||
android:id="@+id/confirmCreateGiftButton"
|
||||
style="@style/Widget.Nova.MaterialButton.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="@string/common_confirm" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:id="@+id/giftsToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
app:homeButtonIcon="@drawable/ic_close"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/giftsList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="80dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/giftsToolbar" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButtonV2
|
||||
android:id="@+id/giftsCreate"
|
||||
style="@style/Widget.Nova.MaterialButton.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/gifts_create"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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">
|
||||
|
||||
<io.novafoundation.nova.common.view.Toolbar
|
||||
android:id="@+id/giftAmountToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:dividerVisible="false"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:id="@+id/giftAmountTitleContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:flexDirection="row"
|
||||
app:flexWrap="wrap"
|
||||
app:layout_constraintTop_toBottomOf="@+id/giftAmountToolbar">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/giftAmountTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Bold.Title2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:includeFontPadding="false"
|
||||
android:text="@string/select_gift_amount_title"
|
||||
android:textColor="@color/text_primary" />
|
||||
|
||||
<io.novafoundation.nova.feature_account_api.view.ChainChipView
|
||||
android:id="@+id/giftAmountChain"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
|
||||
<io.novafoundation.nova.feature_wallet_api.presentation.view.amount.ChooseAmountView
|
||||
android:id="@+id/giftsAmount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/giftAmountTitleContainer" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButton
|
||||
android:id="@+id/giftAmountGetTokens"
|
||||
style="@style/Widget.Nova.Button.AccentSecondaryTransparent.Small"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@+id/giftsAmount"
|
||||
tools:text="Get DOT"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<io.novafoundation.nova.feature_wallet_api.presentation.view.FeeView
|
||||
android:id="@+id/giftAmountFee"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:dividerVisible="false"
|
||||
app:title="@string/common_total_fee"
|
||||
app:layout_constraintTop_toBottomOf="@+id/giftAmountGetTokens" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButtonV2
|
||||
android:id="@+id/giftAmountContinue"
|
||||
style="@style/Widget.Nova.MaterialButton.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="@string/common_continue" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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/shareGiftToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0"
|
||||
app:dividerVisible="false"
|
||||
app:homeButtonIcon="@drawable/ic_close"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:textRight="@string/share_gift_reclaim"
|
||||
app:textRightVisible="false"
|
||||
tools:alpha="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shareGiftTitle"
|
||||
style="@style/TextAppearance.NovaFoundation.Bold.Title1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:alpha="0"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:text="@string/share_gift_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/shareGiftToolbar"
|
||||
tools:alpha="1" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/shareGiftAnimation"
|
||||
android:layout_width="280dp"
|
||||
android:layout_height="280dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/shareGiftAmount"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/shareGiftTitle"
|
||||
app:layout_constraintVertical_bias="0.31"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:lottie_rawRes="@raw/DOT_packing_optimized" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/shareGiftTokenIcon"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:alpha="0"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/shareGiftAmount"
|
||||
app:layout_constraintEnd_toStartOf="@+id/shareGiftAmount"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="@+id/shareGiftAnimation"
|
||||
app:layout_constraintTop_toTopOf="@+id/shareGiftAmount" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shareGiftAmount"
|
||||
style="@style/TextAppearance.NovaFoundation.Bold.LargeTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:alpha="0"
|
||||
android:gravity="center"
|
||||
app:layout_constraintBottom_toTopOf="@+id/shareGiftButton"
|
||||
app:layout_constraintEnd_toEndOf="@+id/shareGiftAnimation"
|
||||
app:layout_constraintStart_toEndOf="@+id/shareGiftTokenIcon"
|
||||
app:layout_constraintTop_toBottomOf="@+id/shareGiftAnimation"
|
||||
tools:alpha="1"
|
||||
tools:text="2 DOT" />
|
||||
|
||||
<io.novafoundation.nova.common.view.PrimaryButtonV2
|
||||
android:id="@+id/shareGiftButton"
|
||||
style="@style/Widget.Nova.MaterialButton.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:alpha="0"
|
||||
android:text="@string/share_gift_button"
|
||||
app:icon="@drawable/ic_share_outline"
|
||||
app:iconGravity="textStart"
|
||||
app:iconTint="@color/icon_primary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:alpha="1" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginVertical="4dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/giftAssetIcon"
|
||||
style="@style/Widget.Nova.AssetIcon.Primary"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:background="@drawable/bg_token_container"
|
||||
android:src="@drawable/ic_star_filled"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:padding="5dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/giftAmount"
|
||||
style="@style/TextAppearance.NovaFoundation.SemiBold.Body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/text_primary"
|
||||
app:layout_constraintBottom_toTopOf="@id/giftCreationDate"
|
||||
app:layout_constraintStart_toEndOf="@+id/giftAssetIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="2 DOT" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/giftAmountChevron"
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:src="@drawable/ic_chevron_right"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/giftAmount"
|
||||
app:layout_constraintStart_toEndOf="@+id/giftAmount"
|
||||
app:layout_constraintTop_toTopOf="@+id/giftAmount" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/giftCreationDate"
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.Footnote"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/text_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/giftAssetIcon"
|
||||
app:layout_constraintTop_toBottomOf="@+id/giftAmount"
|
||||
tools:text="Created: 02.10.2025" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/giftImage"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_gift_packed" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.NovaFoundation.Bold.Title1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/gifts_header_title" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.NovaFoundation.Regular.SubHeadline"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/gifts_header_subtitle"
|
||||
android:textColor="@color/text_secondary" />
|
||||
|
||||
<io.novafoundation.nova.common.view.LinkView
|
||||
android:id="@+id/giftsHeaderLearnMore"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:minHeight="32dp"
|
||||
android:paddingVertical="8dp"
|
||||
app:linkText="@string/common_learn_more" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<io.novafoundation.nova.common.view.InstructionStepView
|
||||
android:id="@+id/giftsInstructionStep1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
app:stepNumber="1" />
|
||||
|
||||
<io.novafoundation.nova.common.view.InstructionStepView
|
||||
android:id="@+id/giftsInstructionStep2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
app:stepNumber="2" />
|
||||
|
||||
<io.novafoundation.nova.common.view.InstructionStepView
|
||||
android:id="@+id/giftsInstructionStep3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
app:stepNumber="3" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:src="@drawable/ic_gifts_placeholder" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.NovaFoundation.SemiBold.Title3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/gifts_list_title" />
|
||||
|
||||
</LinearLayout>
|
||||
Reference in New Issue
Block a user