diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9029856167..f08ee1b88d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { implementation(projects.domain.walletConnect) implementation(projects.domain.markets) implementation(projects.domain.manageTokens) + implementation(projects.domain.onramp) implementation(projects.common) implementation(projects.common.routing) @@ -112,6 +113,7 @@ dependencies { implementation(projects.data.walletConnect) implementation(projects.data.markets) implementation(projects.data.manageTokens) + implementation(projects.data.onramp) /** Features */ implementation(projects.features.onboarding) diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index 30fa60f3c4..67b96dd4a9 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -749,6 +749,16 @@ } ] }, + { + "id": "chiliz", + "name": "Chiliz", + "symbol": "CHZ", + "networks": [ + { + "networkId": "chiliz/test" + } + ] + }, { "id": "casper-network", "name": "Casper", diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index cc4284cc7a..e0669f7e68 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -52,6 +52,7 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.send.api.navigation.SendRouter @@ -59,6 +60,7 @@ import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.operations.backup.BackupService +import com.tangem.sdk.api.BackupServiceHolder import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.extensions.init import com.tangem.tap.common.ActivityResultCallbackHolder @@ -185,6 +187,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase + @Inject + lateinit var backupServiceHolder: BackupServiceHolder + + @Inject + lateinit var onboardingV2FeatureToggles: OnboardingV2FeatureToggles + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -294,7 +302,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac cardSdkOwner.register(activity = this) tangemSdkManager = injectedTangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager - backupService = BackupService.init(cardSdkConfigRepository.sdk, this) + + if (onboardingV2FeatureToggles.isOnboardingV2Enabled) { + backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this) + backupService = backupServiceHolder.backupService.get()!! // will be deleted eventually + } else { + backupService = BackupService.init(cardSdkConfigRepository.sdk, this) + } + lockUserWalletsTimer = LockUserWalletsTimer( owner = this, settingsRepository = settingsRepository, @@ -550,6 +565,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } + // TODO add onboarding v2 handling store.dispatch(BackupAction.CheckForUnfinishedBackup) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index c4366e8215..b8865305c5 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -64,12 +64,16 @@ private fun handleAction(action: Action, appState: () -> AppState?) { } val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card } + val buyService = makeBuyExchangeService(config) + val sellService = makeSellExchangeService(config) val exchangeManager = CurrencyExchangeManager( - buyService = makeBuyExchangeService(config), - sellService = makeSellExchangeService(config), + buyService = buyService, + sellService = sellService, primaryRules = CardExchangeRules(cardProvider), ) // TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance) + store.inject(DaggerGraphState::appStateHolder).buyService = buyService + store.inject(DaggerGraphState::appStateHolder).sellService = sellService store.inject(DaggerGraphState::appStateHolder).exchangeService = exchangeManager store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager)) store.dispatchOnMain(GlobalAction.ExchangeManager.Update) diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index c8d8c5c2bf..f5eb1f355e 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.di +import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.exchange.RampStateManager @@ -10,6 +11,8 @@ import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository import com.tangem.tap.network.exchangeServices.DefaultRampManager import com.tangem.tap.proxy.AppStateHolder +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -39,8 +42,18 @@ internal object ActivityModule { @Provides @Singleton - fun provideDefaultRampManager(appStateHolder: AppStateHolder): RampStateManager { - return DefaultRampManager(appStateHolder.exchangeService) + fun provideDefaultRampManager( + appStateHolder: AppStateHolder, + swapServiceLoader: SwapServiceLoader, + dispatchers: CoroutineDispatcherProvider, + ): RampStateManager { + return DefaultRampManager( + exchangeService = appStateHolder.exchangeService, + buyService = Provider { requireNotNull(appStateHolder.buyService) }, + sellService = Provider { requireNotNull(appStateHolder.sellService) }, + swapServiceLoader = swapServiceLoader, + dispatchers = dispatchers, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkModule.kt new file mode 100644 index 0000000000..b2f2f6d5c5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkModule.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.di + +import com.tangem.sdk.api.BackupServiceHolder +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TangemSdkModule { + + @Provides + @Singleton + fun provideBackupServiceHolder(): BackupServiceHolder = BackupServiceHolder() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt new file mode 100644 index 0000000000..f15fd1b0c0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.onramp.* +import com.tangem.domain.onramp.repositories.OnrampRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object OnrampDomainModule { + + @Provides + @Singleton + fun provideGetOnrampCurrenciesUseCase(onrampRepository: OnrampRepository): GetOnrampCurrenciesUseCase { + return GetOnrampCurrenciesUseCase(onrampRepository) + } + + @Provides + @Singleton + fun provideOnrampSaveDefaultCurrencyUseCase(onrampRepository: OnrampRepository): OnrampSaveDefaultCurrencyUseCase { + return OnrampSaveDefaultCurrencyUseCase(onrampRepository) + } + + @Provides + @Singleton + fun provideGetOnrampCountriesUseCase(onrampRepository: OnrampRepository): GetOnrampCountriesUseCase { + return GetOnrampCountriesUseCase(onrampRepository) + } + + @Provides + @Singleton + fun provideGetOnrampCountryUseCase(onrampRepository: OnrampRepository): GetOnrampCountryUseCase { + return GetOnrampCountryUseCase(onrampRepository) + } + + @Provides + @Singleton + fun provideOnrampSaveDefaultCountryUseCase(onrampRepository: OnrampRepository): OnrampSaveDefaultCountryUseCase { + return OnrampSaveDefaultCountryUseCase(onrampRepository) + } + + @Provides + @Singleton + fun provideCheckOnrampAvailabilityUseCase(onrampRepository: OnrampRepository): CheckOnrampAvailabilityUseCase { + return CheckOnrampAvailabilityUseCase(onrampRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt new file mode 100644 index 0000000000..e94429b646 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.di.domain + +import com.tangem.feature.swap.domain.GetAvailablePairsUseCase +import com.tangem.feature.swap.domain.api.SwapRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** +[REDACTED_AUTHOR] + */ +@Module +@InstallIn(SingletonComponent::class) +internal object SwapDomainModule { + + @Provides + @Singleton + fun provideGetAvailablePairsUseCase(swapRepository: SwapRepository): GetAvailablePairsUseCase { + return GetAvailablePairsUseCase(swapRepository = swapRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 7e547182ce..9bea3ac624 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -7,7 +7,6 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.domain.api.SwapRepository -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -119,26 +118,26 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, swapRepository: SwapRepository, currencyChecksRepository: CurrencyChecksRepository, showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, promoRepository: PromoRepository, stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, + rampStateManager: RampStateManager, ): GetCurrencyWarningsUseCase { return GetCurrencyWarningsUseCase( walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, - marketCryptoCurrencyRepository = marketCryptoCurrencyRepository, currencyChecksRepository = currencyChecksRepository, swapRepository = swapRepository, showSwapPromoTokenUseCase = showSwapPromoTokenUseCase, promoRepository = promoRepository, stakingRepository = stakingRepository, dispatchers = dispatchers, + rampStateManager = rampStateManager, ) } @@ -216,23 +215,19 @@ internal object TokensDomainModule { fun provideGetCryptoCurrencyActionsUseCase( rampStateManager: RampStateManager, walletManagersFacade: WalletManagersFacade, - marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, stakingRepository: StakingRepository, - stakingFeatureToggles: StakingFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( rampManager = rampStateManager, walletManagersFacade = walletManagersFacade, - marketCryptoCurrencyRepository = marketCryptoCurrencyRepository, currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, stakingRepository = stakingRepository, - stakingFeatureToggles = stakingFeatureToggles, dispatchers = dispatchers, ) } diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 444b2c6f89..d4f05a284a 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -17,7 +17,6 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.tap.common.extensions.setContext import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.features.main.model.MainScreenState @@ -40,7 +39,6 @@ internal class MainViewModel @Inject constructor( private val blockchainSDKFactory: BlockchainSDKFactory, private val userWalletsListManager: UserWalletsListManager, private val dispatchers: CoroutineDispatcherProvider, - stakingFeatureToggles: StakingFeatureToggles, private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, private val apiConfigsManager: ApiConfigsManager, homeFeatureToggles: HomeFeatureToggles, @@ -78,9 +76,7 @@ internal class MainViewModel @Inject constructor( displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() - if (stakingFeatureToggles.isStakingEnabled) { - fetchStakingTokens() - } + fetchStakingTokens() deleteDeprecatedLogsUseCase() } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 2f18b059ca..4fc3109010 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -16,6 +16,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding @@ -67,7 +68,10 @@ object OnboardingHelper { fun whereToNavigate(scanResponse: ScanResponse): AppRoute { if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled) { - return AppRoute.Onboarding(scanResponse) + return AppRoute.Onboarding( + scanResponse = scanResponse, + startFromBackup = false, + ) } return when (val type = scanResponse.productType) { @@ -88,6 +92,7 @@ object OnboardingHelper { } fun saveWallet( + alreadyCreatedWallet: UserWallet?, scanResponse: ScanResponse, accessCode: String? = null, backupCardsIds: List? = null, @@ -117,7 +122,7 @@ object OnboardingHelper { // When should not save user wallets but device has biometry and save wallet screen has not been shown, // then open save wallet screen tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> { - proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) + proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet) delay(timeMillis = 1_200) @@ -131,7 +136,7 @@ object OnboardingHelper { } // If device has no biometry and save wallet screen has been shown, then go through old scenario else -> { - proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) + proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet) } } } @@ -242,9 +247,10 @@ object OnboardingHelper { scanResponse: ScanResponse, backupCardsIds: List?, hasBackupError: Boolean, + alreadyCreatedWallet: UserWallet? = null, ) { val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) - val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase) + val userWallet = alreadyCreatedWallet ?: UserWalletBuilder(scanResponse, walletNameGenerateUseCase) .hasBackupError(hasBackupError) .backupCardsIds(backupCardsIds?.toSet()) .build() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index dc9e77a48d..d306ced544 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -196,11 +196,11 @@ private fun handleWalletAction(action: Action) { } } -private fun handleFinishBackup(scanResponse: ScanResponse) { +private fun handleFinishBackup(scanResponse: ScanResponse, userWallet: UserWallet? = null) { val backupState = store.state.onboardingWalletState.backupState val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState) - val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() + val userWalletId = userWallet?.walletId ?: UserWalletIdBuilder.scanResponse(scanResponse).build() if (backupState.hasRing && userWalletId != null) { scope.launch { store.inject(DaggerGraphState::walletsRepository).setHasWalletsWithRing(userWalletId = userWalletId) @@ -208,6 +208,7 @@ private fun handleFinishBackup(scanResponse: ScanResponse) { } OnboardingHelper.saveWallet( + alreadyCreatedWallet = userWallet, scanResponse = updatedScanResponse, accessCode = backupState.accessCode, backupCardsIds = backupState.backupCardIds, @@ -667,7 +668,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) Analytics.send(Onboarding.Finished()) store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds) - handleFinishBackup(requireNotNull(scanResponse)) + handleFinishBackup(requireNotNull(scanResponse), userWallet) delay(1000) store.dispatchWithMain(BackupAction.BackupFinished(userWalletId = userWallet?.walletId)) } @@ -728,6 +729,7 @@ private suspend fun createUserWallet(scanResponse: ScanResponse, backupState: Ba return requireNotNull( value = UserWalletBuilder(scanResponse, walletNameGenerateUseCase) .backupCardsIds(backupState.backupCardIds.toSet()) + .hasBackupError(backupState.hasBackupError) .build(), lazyMessage = { "User wallet not created" }, ) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index a21351a6b9..b68935424e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -55,11 +55,15 @@ object TradeCryptoMiddleware { private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { val isOnrampEnabled = store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled - if (isOnrampEnabled) proceedWithOnramp() else proceedWithLegacyBuyAction(state, action) + if (isOnrampEnabled) { + proceedWithOnramp(action.cryptoCurrencyStatus.currency) + } else { + proceedWithLegacyBuyAction(state, action) + } } - private fun proceedWithOnramp() { - store.dispatchNavigationAction { push(AppRoute.Onramp) } + private fun proceedWithOnramp(cryptoCurrency: CryptoCurrency) { + store.dispatchNavigationAction { push(AppRoute.Onramp(cryptoCurrency)) } } private fun proceedWithLegacyBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index 71c317e080..0e9092a1fa 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -8,6 +8,8 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.domain.common.TapWorkarounds.isTangemTwins +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency @@ -19,6 +21,8 @@ import com.tangem.tap.domain.model.Currency import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import java.math.BigDecimal /** @@ -30,9 +34,19 @@ class CurrencyExchangeManager( private val primaryRules: ExchangeRules, ) : ExchangeService { + override val initializationStatus: StateFlow + get() = _initializationStatus + + private val _initializationStatus: MutableStateFlow = + MutableStateFlow(value = lceLoading()) + override suspend fun update() { + _initializationStatus.value = lceLoading() + buyService.update() sellService.update() + + _initializationStatus.value = lceContent() } override fun isBuyAllowed(): Boolean = primaryRules.isBuyAllowed() && buyService.isBuyAllowed() diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 904e22c80e..b512d32ba7 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -1,15 +1,29 @@ package com.tangem.tap.network.exchangeServices +import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE +import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext -class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager { +internal class DefaultRampManager( + private val exchangeService: ExchangeService?, + private val buyService: Provider, + private val sellService: Provider, + private val swapServiceLoader: SwapServiceLoader, + private val dispatchers: CoroutineDispatcherProvider, +) : RampStateManager { private val cryptoCurrencyConverter = CryptoCurrencyConverter() + override fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean { return exchangeService?.availableForBuy( - scanResponse, + scanResponse = scanResponse, currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), ) ?: false } @@ -19,4 +33,46 @@ class DefaultRampManager(private val exchangeService: ExchangeService?) : RampSt currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), ) ?: false } + + override suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { + return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom + } + + override fun getBuyInitializationStatus(): Flow { + return buyService.invoke().initializationStatus + } + + override suspend fun fetchBuyServiceData() { + withContext(dispatchers.io) { + buyService.invoke().update() + } + } + + override fun getSellInitializationStatus(): Flow { + return sellService.invoke().initializationStatus + } + + override suspend fun fetchSellServiceData() { + withContext(dispatchers.io) { + sellService.invoke().update() + } + } + + override fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow { + return swapServiceLoader.getInitializationStatus(userWalletId) + } + + private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { + return withContext(dispatchers.io) { + val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress + ?: EMPTY_CONTRACT_ADDRESS_VALUE + + val asset = swapServiceLoader.getInitializationStatus(userWalletId).value.getOrNull()?.find { + it.network == cryptoCurrency.network.backendId && + it.contractAddress.equals(contractAddress, ignoreCase = true) + } + + asset?.exchangeAvailable ?: false + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt index 495bcec410..e49b386c9d 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt @@ -1,8 +1,14 @@ package com.tangem.tap.network.exchangeServices +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +typealias ExchangeServiceInitializationStatus = Lce interface Exchanger { fun isBuyAllowed(): Boolean @@ -12,10 +18,17 @@ interface Exchanger { } interface ExchangeService : Exchanger, ExchangeUrlBuilder { + + val initializationStatus: StateFlow + suspend fun update() companion object { fun dummy(): ExchangeService = object : ExchangeService { + + override val initializationStatus: StateFlow = + MutableStateFlow(value = lceLoading()) + override suspend fun update() {} override fun isBuyAllowed(): Boolean = false override fun isSellAllowed(): Boolean = false diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt index 946d2afce1..9a930fdf76 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt @@ -138,6 +138,8 @@ internal val Blockchain.mercuryoNetwork: String? Blockchain.CasperTestnet -> null Blockchain.Core -> null Blockchain.CoreTestnet -> null + Blockchain.Chiliz -> null + Blockchain.ChilizTestnet -> null Blockchain.Unknown -> null Blockchain.Xodex -> null Blockchain.Canxium -> null diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index 50b558907b..bd46579044 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -6,12 +6,19 @@ import com.tangem.common.extensions.calculateSha512 import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.common.services.performRequest +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService +import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList /** @@ -19,10 +26,34 @@ import java.util.concurrent.CopyOnWriteArrayList */ internal class MercuryoService(private val environment: MercuryoEnvironment) : ExchangeService { + override val initializationStatus: StateFlow + get() = _initializationStatus + + private val _initializationStatus: MutableStateFlow = + MutableStateFlow(value = lceLoading()) + private val api: MercuryoApi = environment.mercuryoApi private val availableMercuryoCurrencies = CopyOnWriteArrayList() + override suspend fun update() { + Timber.i("Start updating") + _initializationStatus.value = lceLoading() + + val result = performRequest { api.currencies(environment.apiVersion) } + when { + result is Result.Success && result.data.status == RESPONSE_SUCCESS_STATUS_CODE -> { + handleSuccessfullyUpdatedData(data = result.data.data) + } + result is Result.Failure -> { + availableMercuryoCurrencies.clear() + + Timber.e("Failed to load currencies", result.error) + _initializationStatus.value = result.error.lceError() + } + } + } + override fun isBuyAllowed(): Boolean = true override fun isSellAllowed(): Boolean = false @@ -42,18 +73,6 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E override fun availableForSell(currency: Currency): Boolean = false - override suspend fun update() { - val result = performRequest { api.currencies(environment.apiVersion) } - when { - result is Result.Success && result.data.status == RESPONSE_SUCCESS_STATUS_CODE -> { - handleSuccessfullyUpdatedData(data = result.data.data) - } - result is Result.Failure -> { - availableMercuryoCurrencies.clear() - } - } - } - override fun getUrl( action: CurrencyExchangeManager.Action, cryptoCurrency: CryptoCurrency, @@ -89,6 +108,9 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E private fun handleSuccessfullyUpdatedData(data: MercuryoCurrenciesResponse.Data) { availableMercuryoCurrencies.clear() availableMercuryoCurrencies.addAll(data.config.cryptoCurrencies) + + Timber.i("Successfully updated") + _initializationStatus.value = lceContent() } private fun signature(address: String) = (address + environment.secret).calculateSha512().toHexString().lowercase() diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 26b26f0b61..90011a2d77 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -7,13 +7,20 @@ import com.tangem.common.services.Result import com.tangem.common.services.performRequest import com.tangem.datasource.api.common.createRetrofitInstance import com.tangem.domain.common.extensions.withIOContext +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService +import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import timber.log.Timber import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @@ -23,6 +30,12 @@ class MoonPayService( private val logEnabled: Boolean, ) : ExchangeService { + override val initializationStatus: StateFlow + get() = _initializationStatus + + private val _initializationStatus: MutableStateFlow = + MutableStateFlow(value = lceLoading()) + private val api: MoonPayApi by lazy { createRetrofitInstance( baseUrl = MoonPayApi.MOOONPAY_BASE_URL, @@ -34,14 +47,25 @@ class MoonPayService( override suspend fun update() { withIOContext { + Timber.i("Start updating") + _initializationStatus.value = lceLoading() + performRequest { val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) { - is Result.Failure -> return@performRequest + is Result.Failure -> { + Timber.e("Failed to load user status", result.error) + _initializationStatus.value = result.error.lceError() + return@performRequest + } is Result.Success -> result.data } val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) { - is Result.Failure -> return@performRequest + is Result.Failure -> { + Timber.e("Failed to load currencies", result.error) + _initializationStatus.value = result.error.lceError() + return@performRequest + } is Result.Success -> result.data } @@ -58,6 +82,8 @@ class MoonPayService( ) } + Timber.i("Successfully updated") + _initializationStatus.value = lceContent() status = MoonPayStatus(currenciesToSell, userStatus, currencies) } } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt index fb01fff927..bce26d7a6a 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt @@ -43,6 +43,7 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? Filecoin -> MoonPaySupportedCurrency(networkCode = "filecoin", currencyCode = "fil") Sui -> MoonPaySupportedCurrency(networkCode = "sui", currencyCode = "sui") Core -> MoonPaySupportedCurrency(networkCode = "core", currencyCode = "core") + Chiliz -> MoonPaySupportedCurrency(networkCode = "ethereum", currencyCode = "chz") ArbitrumTestnet -> null AvalancheTestnet -> null BinanceTestnet -> null @@ -139,6 +140,7 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? Casper -> null CasperTestnet -> null CoreTestnet -> null + ChilizTestnet -> null Unknown -> null Xodex -> null Canxium -> null diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index f626aa4238..18c616034a 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -1,6 +1,5 @@ package com.tangem.tap.proxy -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.redux.StateDialog @@ -26,10 +25,8 @@ class AppStateHolder @Inject constructor() : ReduxStateHolder { var mainStore: Store? = null var tangemSdkManager: TangemSdkManager? = null var exchangeService: ExchangeService? = null - - fun getActualCard(): CardDTO? { - return scanResponse?.card - } + var buyService: ExchangeService? = null + var sellService: ExchangeService? = null override fun dispatch(action: Action) { mainStore?.dispatch(action) diff --git a/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt index c3cd524d98..77283b4c75 100644 --- a/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt @@ -27,7 +27,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( override val stack: Value> = childStack( source = navigationProvider.getOrCreateTyped(), - serializer = AppRoute.serializer(), + serializer = AppRoute.serializer(), // TODO Maybe set this to null for AppRoute.Onboarding case. Need to check initialConfiguration = getInitialRoute(), handleBackButton = false, childFactory = ::child, diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index b031d03e46..8678616017 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -11,10 +11,11 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.onramp.component.BuyCryptoComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent +import com.tangem.features.onramp.component.BuyCryptoComponent import com.tangem.features.onramp.component.OnrampComponent import com.tangem.features.onramp.component.SellCryptoComponent +import com.tangem.features.onramp.component.SwapSelectTokensComponent import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter @@ -53,6 +54,7 @@ internal class ChildFactory @Inject constructor( private val onrampComponentFactory: OnrampComponent.Factory, private val buyCryptoComponentFactory: BuyCryptoComponent.Factory, private val sellCryptoComponentFactory: SellCryptoComponent.Factory, + private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory, private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, @@ -197,7 +199,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.Onramp -> { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = OnrampComponent.Params(), + params = OnrampComponent.Params(route.currency.name), componentFactory = onrampComponentFactory, ) } @@ -215,10 +217,20 @@ internal class ChildFactory @Inject constructor( componentFactory = sellCryptoComponentFactory, ) } + is AppRoute.SwapCrypto -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId), + componentFactory = swapSelectTokensComponentFactory, + ) + } is AppRoute.Onboarding -> { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = OnboardingEntryComponent.Params(route.scanResponse), + params = OnboardingEntryComponent.Params( + scanResponse = route.scanResponse, + startBackupFlow = route.startFromBackup, + ), componentFactory = onboardingEntryComponentFactory, ) } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 6744b6c0be..66993b210a 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -219,16 +219,24 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Swap( - val currency: CryptoCurrency, + val currencyFrom: CryptoCurrency, + val currencyTo: CryptoCurrency? = null, val userWalletId: UserWalletId, val isInitialReverseOrder: Boolean = false, - ) : AppRoute(path = "/swap/${currency.id.value}/${userWalletId.stringValue}/$isInitialReverseOrder"), + ) : AppRoute( + path = "/swap" + + "/${currencyFrom.id.value}" + + "/${currencyTo?.id?.value}" + + "/${userWalletId.stringValue}" + + "/$isInitialReverseOrder", + ), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) companion object { - const val CURRENCY_BUNDLE_KEY = "currency" + const val CURRENCY_FROM_KEY = "currencyFrom" + const val CURRENCY_TO_KEY = "currencyTo" const val USER_WALLET_ID_KEY = "userWalletId" const val IS_INITIAL_REVERSE_ORDER = "isInitialReverseOrder" } @@ -287,7 +295,9 @@ sealed class AppRoute(val path: String) : Route { } @Serializable - data object Onramp : AppRoute(path = "/onramp") + data class Onramp(val currency: CryptoCurrency) : AppRoute(path = "/onramp/${currency.symbol}"), RouteBundleParams { + override fun getBundle(): Bundle = bundle(serializer()) + } @Serializable data class BuyCrypto( @@ -299,6 +309,14 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, ) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}") + @Serializable + data class SwapCrypto( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}") + // Onboarding V2 - data class Onboarding(val scanResponse: ScanResponse) : AppRoute(path = "/onboarding_v2") + data class Onboarding( + val scanResponse: ScanResponse, + val startFromBackup: Boolean, + ) : AppRoute(path = "/onboarding_v2${if (startFromBackup) "/backup" else ""}") } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index ae4993d6ce..62e5cb7ed4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent @@ -39,8 +40,8 @@ class TokenItemStateConverter( private val fiatAmountStateProvider: (CryptoCurrencyStatus) -> TokenItemState.FiatAmountState? = { createFiatAmountState(it, appCurrency) }, - private val onItemClick: (CryptoCurrencyStatus) -> Unit, - private val onItemLongClick: ((CryptoCurrencyStatus) -> Unit)? = null, + private val onItemClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null, + private val onItemLongClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null, ) : Converter { override fun convert(value: CryptoCurrencyStatus): TokenItemState { @@ -75,9 +76,11 @@ class TokenItemStateConverter( subtitleState = requireNotNull(subtitleStateProvider(this)), fiatAmountState = requireNotNull(fiatAmountStateProvider(this)), subtitle2State = TokenItemState.Subtitle2State.TextContent(text = getFormattedAmount()), - onItemClick = { onItemClick(this) }, - onItemLongClick = onItemLongClick?.let { - { it(this) } + onItemClick = onItemClick?.let { onItemClick -> + { onItemClick(it, this) } + }, + onItemLongClick = onItemLongClick?.let { onItemLongClick -> + { onItemLongClick(it, this) } }, ) } @@ -94,9 +97,11 @@ class TokenItemStateConverter( iconState = iconStateProvider(this), titleState = titleStateProvider(this), subtitleState = subtitleStateProvider(this), - onItemClick = { onItemClick(this) }, - onItemLongClick = onItemLongClick?.let { - { it(this) } + onItemClick = onItemClick?.let { onItemClick -> + { onItemClick(it, this) } + }, + onItemLongClick = onItemLongClick?.let { onItemLongClick -> + { onItemLongClick(it, this) } }, ) } @@ -107,8 +112,8 @@ class TokenItemStateConverter( iconState = iconStateProvider(this), titleState = titleStateProvider(this), subtitleState = subtitleStateProvider(this), - onItemLongClick = onItemLongClick?.let { - { it(this) } + onItemLongClick = onItemLongClick?.let { onItemLongClick -> + { onItemLongClick(it, this) } }, ) } @@ -122,7 +127,7 @@ class TokenItemStateConverter( is CryptoCurrencyStatus.Unreachable, is CryptoCurrencyStatus.NoAmount, -> { - TokenItemState.TitleState.Content(text = currencyStatus.currency.name) + TokenItemState.TitleState.Content(text = stringReference(currencyStatus.currency.name)) } is CryptoCurrencyStatus.Loaded, is CryptoCurrencyStatus.Custom, @@ -130,7 +135,7 @@ class TokenItemStateConverter( is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.TitleState.Content( - text = currencyStatus.currency.name, + text = stringReference(currencyStatus.currency.name), hasPending = value.hasCurrentNetworkTransactions, ) } diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 494f2c12e6..273078fac5 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.libs.auth) implementation(projects.domain.appTheme.models) + implementation(projects.domain.core) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.balanceHiding.models) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/MockedOnrampApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/MockedOnrampApi.kt deleted file mode 100644 index a960766de8..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/MockedOnrampApi.kt +++ /dev/null @@ -1,178 +0,0 @@ -package com.tangem.datasource.api.onramp - -import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO -import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest -import com.tangem.datasource.api.onramp.models.response.OnrampDataResponse -import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse -import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse -import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO -import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO -import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO -import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO - -internal class MockedOnrampApi : OnrampApi { - override suspend fun getCurrencies(): ApiResponse> = ApiResponse.Success( - COUNTRIES.map(OnrampCountryDTO::defaultCurrency), - ) - - override suspend fun getCountries(): ApiResponse> = ApiResponse.Success(COUNTRIES + RUSSIA) - - override suspend fun getCountryByIp(): ApiResponse = ApiResponse.Success(RUSSIA) - - override suspend fun getPaymentMethods(): ApiResponse> = ApiResponse.Success( - listOf( - PaymentMethodDTO(id = "google", name = "Google Play", image = ""), - PaymentMethodDTO(id = "apple", name = "Apple Pay", image = ""), - PaymentMethodDTO(id = "card", name = "Card", image = ""), - ), - ) - - override suspend fun getPairs(body: OnrampPairsRequest): ApiResponse> = ApiResponse.Success( - listOf( - OnrampPairDTO( - fromCurrencyCode = "USD", - to = OnrampDestinationDTO(contractAddress = "0xcontract_address", network = "ethereum"), - providers = listOf(), - ), - ), - ) - - override suspend fun getQuote( - fromCurrencyCode: String, - toContractAddress: String, - toNetwork: String, - paymentMethod: String, - countryCode: String, - fromAmount: String, - toDecimals: Int, - providerId: String, - ): ApiResponse { - TODO("Not yet implemented") - } - - override suspend fun getData( - fromCurrencyCode: String, - toContractAddress: String, - toNetwork: String, - paymentMethod: String, - countryCode: String, - fromAmount: String, - toDecimals: Int, - providerId: String, - toAddress: String, - redirectUrl: String, - language: String?, - theme: String?, - requestId: String, - ): ApiResponse { - TODO("Not yet implemented") - } - - override suspend fun getStatus(txId: String): ApiResponse { - TODO("Not yet implemented") - } - - private companion object { - private val RUSSIA = OnrampCountryDTO( - name = "Russia", - code = "RU", - image = "https://hatscripts.github.io/circle-flags/flags/ru.svg", - alpha3 = "RUS", - continent = "", - defaultCurrency = OnrampCurrencyDTO( - name = "Russian ruble", - code = "RUB", - image = "https://hatscripts.github.io/circle-flags/flags/ru.svg", - precision = 2, - ), - onrampAvailable = false, - ) - private val COUNTRIES = listOf( - OnrampCountryDTO( - name = "United States of America", - code = "USA", - image = "https://hatscripts.github.io/circle-flags/flags/us.svg", - alpha3 = "USA", - continent = "", - defaultCurrency = OnrampCurrencyDTO( - name = "US Dollar", - code = "USD", - image = "https://hatscripts.github.io/circle-flags/flags/us.svg", - precision = 2, - ), - onrampAvailable = true, - ), - OnrampCountryDTO( - name = "Europe Union", - code = "EU", - image = "https://hatscripts.github.io/circle-flags/flags/eu.svg", - alpha3 = "EUR", - continent = "", - defaultCurrency = OnrampCurrencyDTO( - name = "Euro", - code = "EUR", - image = "https://hatscripts.github.io/circle-flags/flags/eu.svg", - precision = 2, - ), - onrampAvailable = true, - ), - OnrampCountryDTO( - name = "Great Britain", - code = "GB", - image = "https://hatscripts.github.io/circle-flags/flags/gb.svg", - alpha3 = "GB", - continent = "", - defaultCurrency = OnrampCurrencyDTO( - name = "British Pound Sterling", - code = "GBP", - image = "https://hatscripts.github.io/circle-flags/flags/gb.svg", - precision = 2, - ), - onrampAvailable = true, - ), - OnrampCountryDTO( - name = "CANADA", - code = "CA", - image = "https://hatscripts.github.io/circle-flags/flags/ca.svg", - alpha3 = "CA", - continent = "", - defaultCurrency = OnrampCurrencyDTO( - name = "Canadian Dollar", - code = "CAD", - image = "https://hatscripts.github.io/circle-flags/flags/ca.svg", - precision = 2, - ), - onrampAvailable = true, - ), - OnrampCountryDTO( - name = "Hon Kong", - code = "HK", - image = "https://hatscripts.github.io/circle-flags/flags/hk.svg", - alpha3 = "HK", - continent = "", - defaultCurrency = OnrampCurrencyDTO( - name = "Hon Kong Dollar", - code = "HKD", - image = "https://hatscripts.github.io/circle-flags/flags/hk.svg", - precision = 2, - ), - onrampAvailable = true, - ), - OnrampCountryDTO( - name = "Australia", - code = "AU", - image = "https://hatscripts.github.io/circle-flags/flags/au.svg", - alpha3 = "AU", - continent = "", - defaultCurrency = OnrampCurrencyDTO( - name = "Australian Dollar", - code = "AUD", - image = "https://hatscripts.github.io/circle-flags/flags/au.svg", - precision = 2, - ), - onrampAvailable = true, - ), - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 720e14ac0b..8b38ca983a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -11,7 +11,6 @@ import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.markets.TangemTechMarketsApi -import com.tangem.datasource.api.onramp.MockedOnrampApi import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -99,24 +98,22 @@ internal object NetworkModule { @Provides @Singleton fun provideOnrampApi( - // @NetworkMoshi moshi: Moshi, - // @ApplicationContext context: Context, - // apiConfigsManager: ApiConfigsManager, - // appLogsStore: AppLogsStore, + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + apiConfigsManager: ApiConfigsManager, + appLogsStore: AppLogsStore, ): OnrampApi { - // TODO: Remove when backend will be ready - [REDACTED_TASK_KEY] - return MockedOnrampApi() - // return createApi( - // id = ApiConfig.ID.Express, - // moshi = moshi, - // context = context, - // apiConfigsManager = apiConfigsManager, - // clientBuilder = { - // addInterceptor( - // NetworkLogsSaveInterceptor(appLogsStore), - // ) - // }, - // ) + return createApi( + id = ApiConfig.ID.Express, + moshi = moshi, + context = context, + apiConfigsManager = apiConfigsManager, + clientBuilder = { + addInterceptor( + NetworkLogsSaveInterceptor(appLogsStore), + ) + }, + ) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/ExchangeServiceLoaderModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/ExchangeServiceLoaderModule.kt new file mode 100644 index 0000000000..03ab7fa98a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/ExchangeServiceLoaderModule.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.di.exchangeservice + +import com.tangem.datasource.exchangeservice.swap.DefaultSwapServiceLoader +import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ExchangeServiceLoaderModule { + + @Binds + @Singleton + fun bindSwapServiceLoader(defaultSwapServiceLoader: DefaultSwapServiceLoader): SwapServiceLoader +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultSwapServiceLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultSwapServiceLoader.kt new file mode 100644 index 0000000000..90a267cc3d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultSwapServiceLoader.kt @@ -0,0 +1,91 @@ +package com.tangem.datasource.exchangeservice.swap + +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE +import com.tangem.datasource.api.express.models.request.AssetsRequestBody +import com.tangem.datasource.api.express.models.request.LeastTokenInfo +import com.tangem.datasource.api.express.models.response.Asset +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.token.ExpressAssetsStore +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +typealias InitializationStatusFlow = MutableStateFlow>> + +/** + * Default implementation of [SwapServiceLoader] + * + * @property tangemExpressApi express api + * @property expressAssetsStore local storage + * +[REDACTED_AUTHOR] + */ +internal class DefaultSwapServiceLoader @Inject constructor( + private val tangemExpressApi: TangemExpressApi, + private val expressAssetsStore: ExpressAssetsStore, + private val dispatchers: CoroutineDispatcherProvider, +) : SwapServiceLoader { + + private val initializationStatuses = + MutableStateFlow>(value = emptyMap()) + + override suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse) { + withContext(dispatchers.io) { + val initializationStatus = getInitializationStatusInternal(userWalletId) + + initializationStatus.update { lceLoading() } + + try { + val tokensList = userTokens.tokens.map { + LeastTokenInfo( + contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, + network = it.networkId, + ) + } + + if (tokensList.isNotEmpty()) { + val response = tangemExpressApi.getAssets( + body = AssetsRequestBody(tokensList = tokensList), + ).getOrThrow() + + expressAssetsStore.store(userWalletId, response) + + initializationStatus.update { response.lceContent() } + } + } catch (e: Throwable) { + initializationStatus.update { e.lceError() } + Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") + } + } + } + + override fun getInitializationStatus(userWalletId: UserWalletId): InitializationStatusFlow { + return getInitializationStatusInternal(userWalletId) + } + + private fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow { + val initializationStatus = initializationStatuses.value.get(key = userWalletId) + + if (initializationStatus != null) return initializationStatus + + val default: InitializationStatusFlow = MutableStateFlow(value = lceLoading()) + + initializationStatuses.update { + it.toMutableMap().apply { + put(key = userWalletId, value = default) + } + } + + return default + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/SwapServiceLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/SwapServiceLoader.kt new file mode 100644 index 0000000000..0bee5e6d0b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/SwapServiceLoader.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.exchangeservice.swap + +import com.tangem.datasource.api.express.models.response.Asset +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.StateFlow + +/** + * Swap service loader + * +[REDACTED_AUTHOR] + */ +interface SwapServiceLoader { + + /** Update service using [userWalletId] and [userTokens] */ + suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse) + + /** Get initialization status by [userWalletId] */ + fun getInitializationStatus(userWalletId: UserWalletId): StateFlow>> +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 10f76c5e66..a049a7db22 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -103,6 +103,10 @@ object PreferencesKeys { val SHOULD_SHOW_RING_PROMO_KEY by lazy { booleanPreferencesKey(name = "shouldShowRingPromo") } + val ONRAMP_DEFAULT_CURRENCY by lazy { stringPreferencesKey(name = "onrampDefaultCurrency") } + + val ONRAMP_DEFAULT_COUNTRY by lazy { stringPreferencesKey(name = "onrampDefaultCountry") } + // region Permission fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission") diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 2751047561..f7af0b38fc 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -7,10 +7,6 @@ "name": "WC_SOLANA_TX_SIGN_ENABLED", "version": "5.18.0" }, - { - "name": "STAKING_ENABLED", - "version": "5.15.0" - }, { "name": "IS_ETHEREUM_EIP_1559_ENABLED", "version": "5.17.0" @@ -25,7 +21,7 @@ }, { "name": "MAIN_ACTION_BUTTONS_ENABLED", - "version": "undefined" + "version": "5.19.0" }, { "name": "ONBOARDING_CODE_REFACTORING_ENABLED", diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index db671f2128..5e53fcf4cc 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -6,6 +6,8 @@ Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es für Swaps zu finden und hinzuzufügen Es gibt keine verfügbaren Token, die mit dem ausgewählten Token getauscht werden können. Bitte wähle einen anderen. Kein verfügbares Paar + Du möchtest erhalten + Du möchtest tauschen Netzwerk wählen Token anlegen Token verwalten @@ -260,7 +262,7 @@ Abgebrochen Bestätigt Bestätigen - wird bestätigt... + Wird bestätigt... Getauscht Tauschen läuft Austauschen... @@ -451,6 +453,7 @@ Preisleistung Aufbewahrungsort Sicherheitsbewertung + Der Security Score eines Tokens ist eine Metrik, die das Sicherheitsniveau einer Blockchain oder eines Tokens anhand verschiedener Faktoren bewertet und aus den unten aufgeführten Quellen zusammengestellt wird. Soziales Gesamtangebot Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können @@ -675,7 +678,7 @@ Aufgrund der Besonderheiten des Netzes %1$s ist die Gebühr für die Überweisung des gesamten Guthabens höher. Um die Kommission zu reduzieren, Kannst du %2$s verlassen. Die Gebühr ist höher Die enthaltene Kommission übersteigt den Überweisungsbetrag, was zu einem negativen Wert führt - ungültige Menge + Ungültige Menge Der Mindestbetrag für den Versand beträgt %1$s. Bitte stell sicher, dass der Restbetrag nach dem Versand nicht unter %2$s liegt. Das Zielkonto wurde nicht erstellt. Bitte änder den zu sendenden Betrag. Der zu sendende Betrag muss mindestens %s betragen @@ -804,7 +807,7 @@ Lösen der Bindungen Gelocktes unlocken Entsperren - Der unstaking-Betrag muss mindestens %s betragen. + Der unstaking-Betrag muss mindestens %s betragen Der Betrag übersteigt das eingesetzte Guthaben Unstaken Staking beenden @@ -884,6 +887,7 @@ von: %s zu: %s Validierer: %s + Minimum %s Der Mindesttransaktionsbetrag beträgt %1$s. Versuche es erneut Du hast dieselbe Karte oder Ring gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte oder Ring mit der Nummer %d scannen. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 8c35a676fa..f8405d31f3 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -6,6 +6,8 @@ ¿No encuentras el token en tu billetera? Consulte los mercados para encontrarlo y agregarlo al intercambio No hay tokens disponibles para intercambiar con el token seleccionado. Por favor elige otro. No hay par disponible + Desea recibir + Quiere intercambiar Elige red Agregue un token personalizado Gestionar tokens @@ -740,7 +742,7 @@ Periodo de calentamiento El tiempo permitido para activar la participación en la apuesta. La red cobrará una tarifa de aprobación de token para verificar que usted está autorizando el uso de su token para el staking. - Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor + Al utilizar la función de staking, usted acepta %1$s y %2$s del proveedor Bloqueado Migrar Native staking @@ -802,7 +804,7 @@ Desunión Desbloquear Desbloqueando - El monto del staking debe ser al menos %s + La cantidad para el staking debe ser al menos %s El monto excede el saldo apostado Sin staking Unstaking diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index cfb3abf10e..dba4e36c8b 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -6,6 +6,8 @@ Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à l\'échange Il n\'y a pas de token disponible pour échanger avec le token sélectionné. Veuillez en choisir un autre. Aucune paire disponible + Vous souhaitez recevoir + Vous souhaitez échanger Choisissez le réseau Ajouter un jeton personnalisé Gérer les jetons @@ -740,7 +742,7 @@ Période d\'échauffement Le temps imparti pour activer la participation au staking. Le réseau facturera des frais d’approbation de jeton pour vérifier que vous autorisez l’utilisation de votre jeton pour le jalonnement. - En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur + En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur Bloqué Migrer Native staking @@ -802,7 +804,7 @@ Dissociation Débloquer Déverrouillage - Le montant à staker doit être au moins %s + Le montant à destaker doit être au moins %s Le montant dépasse le solde misé Non-staké Unstaking diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 58687b9a15..af02575293 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -6,6 +6,8 @@ ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、スワップのために追加してください 選択したトークンと交換できるトークンがありません。別のトークンを選択してください。 利用可能なペアがありません + 受け取りたい + 交換したい ネットワークを選択 カスタムトークンの追加 トークンの管理 @@ -445,6 +447,7 @@ 値動き リポジトリ セキュリティ・スコア + トークンのセキュリティ・スコアとは、ブロックチェーンやトークンのセキュリティ・レベルを様々な要因に基づいて評価する指標で、以下の情報源から集計されます。 ソーシャル 総供給量 特定の暗号資産に存在しうるコインまたはトークンの最大数 @@ -794,7 +797,7 @@ ステーキング解約中 ロック解除 ロック解除中 - ステーキング金額は %s 以上である必要があります + ステーキング解除する金額は少なくとも%sである必要があります 金額がステーキング残高を超えています ステーキングされていない ステーキング解除 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index babcd2542e..952af02796 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -3,6 +3,8 @@ Выберите токен Нет доступных токенов для обмена с выбранным токеном, пожалуйста, выберите другой. Нет доступных пар + Вы хотите получить + Вы хотите обменять Выберите сеть Добавить токен Валюты @@ -311,7 +313,7 @@ Транзакция Укажите лимит доступа к выбранному токену Количество %s - Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов.По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена. + Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена. Чтобы продолжить, вам необходимо разрешить смарт контракту Polygon использовать ваш %s Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s Дать разрешение @@ -768,6 +770,7 @@ Стейкайте безопасно и начинайте получать еженедельные награды. Получите награду за стейкинг Оставшийся застейканный баланс будет слишком мал для вывода. Вам потребуется застейкать больше средств, чтобы достичь минимальной суммы для вывода. + Низкий баланс стейкинга Стейкинг временно недоступен из-за проблем в сети. Пожалуйста, попробуйте позже. Стейкинг в сети %1$s с новым валидатором автоматически переведет ваши текущие застейканные средства на него. Реинвестируйте свои заработанные награды в вашу застейканную сумму, увеличивая потенциальный доход @@ -800,6 +803,7 @@ Вознаграждения Стейкинг закрыт Застейкать еще + Застейканная сумма Вы стейкаете %1$s и будете получать награду %2$s Нажмите для разблокировки Нажмите для разблокировки @@ -933,7 +937,7 @@ Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. Неверная карта или кольцо выбрана в приложении Tangem Не удалось создать транзакцию из данных Dapp. Код: %s - Произошла непредвиденная ошибка. Код ошибки: %d. Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. + Произошла непредвиденная ошибка. Код ошибки: %d. Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать — обратитесь в службу поддержки. Нет открытых сессий WalletConnect Упс. Нет сессий. Не удалось создать пару WalletConnect: %1$s @@ -988,7 +992,7 @@ Сумма получения не может быть менее %s Это может произойти из-за того, что провайдер временно не предоставляет обмен выбранной вами пары. Пожалуйста, подождите некоторое время и попробуйте снова. (Код %s) Выбранная пара временно недоступна - Cервис временно недоступен + Сервис временно недоступен Сумма для обмена должна быть не более %s Сумма для обмена должна быть не менее %s Пожалуйста, измените сумму для обмена diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 53de8082e3..c3f58becbb 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -6,6 +6,8 @@ Токен не знайдено у вашому портфелі? Перевірте ринки, щоб знайти та додати його для обміну Немає доступних токенів для обміну з обраним токеном. Будь ласка, оберіть інший. Немає вільної пари + Ви хочете отримати + Ви хочете обміняти Оберіть мережу Додати токен Токени @@ -732,7 +734,7 @@ Сума зняття зі стейкінгу буде округлена до %1$s TRX через мережеві правила. Зняти кошти Комісія за стейкінг-акаунт - Стейкінг-акаунт - це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу. + Стейкінг-акаунт — це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу. Процентна ставка Річний відсоток, який ви можете отримати, беручи участь у стейкінгу. APR @@ -818,7 +820,6 @@ Розблокування Розблокувати Розблокування - Сума для стейкінгу має бути не менше %s Сума перевищує баланс стейкінгу Вивід зі стейкінгу Зняти зі стейкінгу diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index ef0cd2f4f5..2592e85ed6 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -6,6 +6,8 @@ Token not found in your portfolio? Check the Markets to find and add it for swap There are no available tokens to swap with the selected token. Please choose another one. No available pair + You want to Receive + You want to Swap Choose network Add custom token Manage tokens @@ -451,6 +453,7 @@ Price performance Repository Security score + Security score of a token is a metric that assesses the security level of a blockchain or token based on various factors and is compiled from the sources listed below. Social Total supply The maximum number of coins or tokens that can ever exist for a particular cryptocurrency diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt index 65304bcf57..bd95803a6a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt @@ -4,6 +4,7 @@ import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM @@ -26,6 +27,7 @@ fun AppBarWithBackButton( modifier: Modifier = Modifier, text: String? = null, @DrawableRes iconRes: Int? = null, + containerColor: Color = Color.Transparent, ) { TangemTopAppBar( modifier = modifier, @@ -34,6 +36,7 @@ fun AppBarWithBackButton( iconRes = iconRes ?: R.drawable.ic_back_24, onIconClicked = onBackClick, ), + containerColor = containerColor, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt index c93d92643e..d315a2a715 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt @@ -23,4 +23,5 @@ data class ActionButtonConfig( val onLongClick: (() -> TextReference?)? = null, val enabled: Boolean = true, val dimContent: Boolean = false, + val isInProgress: Boolean = false, ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 60bc6e7e12..728eb3a49c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -8,8 +8,9 @@ import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -77,7 +78,6 @@ fun ActionButton( ) } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun Button( config: ActionButtonConfig, @@ -89,12 +89,33 @@ private fun Button( targetValue = if (config.enabled) color else TangemTheme.colors.button.disabled, label = "Update background color", ) - val context = LocalContext.current - Row( + + Box( modifier = modifier .heightIn(min = TangemTheme.dimens.size36) .clip(shape) - .background(color = backgroundColor) + .background(color = backgroundColor), + ) { + Content(config = config) + + if (config.isInProgress) { + Loading( + backgroundColor = backgroundColor, + modifier = Modifier + .matchParentSize() + .align(alignment = Alignment.Center), + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun Content(config: ActionButtonConfig) { + val context = LocalContext.current + + Row( + modifier = Modifier .combinedClickable( enabled = config.enabled, onClick = config.onClick, @@ -149,6 +170,19 @@ private fun Button( } } +@Composable +private fun Loading(backgroundColor: Color, modifier: Modifier = Modifier) { + Box( + modifier = modifier.background(color = backgroundColor), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.accent, + ) + } +} + @Preview(group = "RoundedActionButton", showBackground = true) @Preview(group = "RoundedActionButton", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -188,5 +222,12 @@ private class ActionStateProvider : CollectionPreviewParameterProvider { val onClick = state.onItemClick - val onLongClick = state.onItemLongClick?.let { rememberHapticFeedback(state = state, onAction = it) } + val onLongClick = state.onItemLongClick?.let { + rememberHapticFeedback(state = state, onAction = { it(state) }) + } when { onClick == null && onLongClick == null -> this onClick == null && onLongClick != null -> combinedClickable(onClick = {}, onLongClick = onLongClick) - onClick != null && onLongClick == null -> combinedClickable(onClick = onClick) + onClick != null && onLongClick == null -> combinedClickable(onClick = { onClick(state) }) onClick != null && onLongClick != null -> { - combinedClickable(onClick = onClick, onLongClick = onLongClick) + combinedClickable(onClick = { onClick(state) }, onLongClick = onLongClick) } else -> this } @@ -456,7 +459,7 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider { - PriceText(text = state.value, modifier = modifier, isAvailable = state.isAvailable) + PriceText(text = state.value.resolveReference(), modifier = modifier, isAvailable = state.isAvailable) } is TokenPriceState.Unknown -> { PriceText(text = DASH_SIGN, modifier = modifier) @@ -158,7 +160,7 @@ private class TokenPriceChangeStateProvider : CollectionPreviewParameterProvider priceChangePercent = "2.5%", type = PriceChangeType.NEUTRAL, ), - TokenPriceState.TextContent(value = "Subtitle", isAvailable = true), + TokenPriceState.TextContent(value = stringReference(value = "Subtitle"), isAvailable = true), TokenPriceState.Unknown, TokenPriceState.Loading, TokenPriceState.Locked, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt index 9bf3412f33..7f379e09a6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.components.token.state.TokenItemState.TitleState as TokenTitleState @@ -46,7 +47,7 @@ private fun ContentTitle(state: TokenTitleState.Content, modifier: Modifier = Mo * So we need to use [weight] to avoid displacement. */ CurrencyNameText( - name = state.text, + name = state.text.resolveReference(), isAvailable = state.isAvailable, modifier = Modifier.weight(weight = 1f, fill = false), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index c33e5c8caa..6f3b2342a9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.audits.AuditLabelUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference /** TokenItem component state */ @Immutable @@ -31,10 +32,10 @@ sealed class TokenItemState { abstract val subtitle2State: Subtitle2State? /** Callback which will be called when an item is clicked */ - abstract val onItemClick: (() -> Unit)? + abstract val onItemClick: ((TokenItemState) -> Unit)? /** Callback which will be called when an item is long clicked */ - abstract val onItemLongClick: (() -> Unit)? + abstract val onItemLongClick: ((TokenItemState) -> Unit)? /** * Loading token state @@ -52,8 +53,8 @@ sealed class TokenItemState { ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading override val subtitle2State: Subtitle2State = Subtitle2State.Loading - override val onItemClick: (() -> Unit)? = null - override val onItemLongClick: (() -> Unit)? = null + override val onItemClick: ((TokenItemState) -> Unit)? = null + override val onItemLongClick: ((TokenItemState) -> Unit)? = null } /** @@ -67,8 +68,8 @@ sealed class TokenItemState { override val subtitleState: SubtitleState = SubtitleState.Locked override val fiatAmountState: FiatAmountState = FiatAmountState.Locked override val subtitle2State: Subtitle2State = Subtitle2State.Locked - override val onItemClick: (() -> Unit)? = null - override val onItemLongClick: (() -> Unit)? = null + override val onItemClick: ((TokenItemState) -> Unit)? = null + override val onItemLongClick: ((TokenItemState) -> Unit)? = null } /** @@ -90,8 +91,8 @@ sealed class TokenItemState { override val subtitleState: SubtitleState, override val fiatAmountState: FiatAmountState, override val subtitle2State: Subtitle2State, - override val onItemClick: (() -> Unit)?, - override val onItemLongClick: (() -> Unit)?, + override val onItemClick: ((TokenItemState) -> Unit)?, + override val onItemLongClick: ((TokenItemState) -> Unit)?, ) : TokenItemState() /** @@ -110,8 +111,8 @@ sealed class TokenItemState { ) : TokenItemState() { override val subtitleState: SubtitleState? = null override val fiatAmountState: FiatAmountState? = null - override val onItemClick: (() -> Unit)? = null - override val onItemLongClick: (() -> Unit)? = null + override val onItemClick: ((TokenItemState) -> Unit)? = null + override val onItemLongClick: ((TokenItemState) -> Unit)? = null } /** @@ -129,8 +130,8 @@ sealed class TokenItemState { override val iconState: CurrencyIconState, override val titleState: TitleState, override val subtitleState: SubtitleState? = null, - override val onItemClick: (() -> Unit)?, - override val onItemLongClick: (() -> Unit)?, + override val onItemClick: ((TokenItemState) -> Unit)?, + override val onItemLongClick: ((TokenItemState) -> Unit)?, ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val subtitle2State: Subtitle2State? = null @@ -150,18 +151,18 @@ sealed class TokenItemState { override val iconState: CurrencyIconState, override val titleState: TitleState, override val subtitleState: SubtitleState? = null, - override val onItemLongClick: (() -> Unit)?, + override val onItemLongClick: ((TokenItemState) -> Unit)?, ) : TokenItemState() { override val fiatAmountState: FiatAmountState? = null override val subtitle2State: Subtitle2State? = null - override val onItemClick: (() -> Unit)? = null + override val onItemClick: ((TokenItemState) -> Unit)? = null } @Immutable sealed class TitleState { data class Content( - val text: String, + val text: TextReference, val hasPending: Boolean = false, val isAvailable: Boolean = true, ) : TitleState() @@ -180,7 +181,7 @@ sealed class TokenItemState { val type: PriceChangeType, ) : SubtitleState() - data class TextContent(val value: String, val isAvailable: Boolean = true) : SubtitleState() + data class TextContent(val value: TextReference, val isAvailable: Boolean = true) : SubtitleState() data object Unknown : SubtitleState() diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index 56b0ce8bf1..b76e11c37c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem import com.tangem.core.ui.components.tokenlist.internal.NetworkTitleItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resolveReference @@ -25,6 +26,9 @@ fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: M is TokensListItemUM.NetworkGroupTitle -> { NetworkTitleItem(networkName = state.name.resolveReference(), modifier = modifier) } + is TokensListItemUM.GroupTitle -> { + GroupTitleItem(state, modifier) + } is TokensListItemUM.Token -> { TokenItem( state = state.state, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/GroupTitleItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/GroupTitleItem.kt new file mode 100644 index 0000000000..c79e40c723 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/GroupTitleItem.kt @@ -0,0 +1,31 @@ +package com.tangem.core.ui.components.tokenlist.internal + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.rows.NetworkTitle +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Group title item + * + * @param state state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@Composable +fun GroupTitleItem(state: TokensListItemUM.GroupTitle, modifier: Modifier = Modifier) { + NetworkTitle( + title = { + Text( + text = state.text.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + modifier = modifier, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt index 4f6bf25949..8ca65a2982 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt @@ -4,7 +4,6 @@ import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -78,9 +77,7 @@ private fun BaseNetworkTitleItem( @Composable private fun DraggableIcon(reorderableTokenListState: ReorderableLazyListState) { Box( - modifier = Modifier - .size(TangemTheme.dimens.size32) - .detectReorder(reorderableTokenListState), + modifier = Modifier.detectReorder(reorderableTokenListState), contentAlignment = Alignment.Center, ) { Icon( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt index 8a26f00508..2e9417d5ed 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt @@ -29,8 +29,17 @@ sealed interface TokensListItemUM { * @property id id * @property name network group name */ + @Deprecated("Use GroupTitle instead") // TODO: [REDACTED_JIRA] data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemUM + /** + * Group title + * + * @property id id + * @property text title value + */ + data class GroupTitle(override val id: Any, val text: TextReference) : TokensListItemUM + /** * Token item * diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 28de4a0d24..5aa40111d8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -82,6 +82,7 @@ fun getActiveIconRes(blockchainId: String): Int { "casper", "casper/test" -> R.drawable.img_casper_22 "xodex" -> R.drawable.img_xodex_22 "canxium" -> R.drawable.img_canxium_22 + "chiliz", "chiliz/test" -> R.drawable.img_chiliz_22 else -> R.drawable.ic_alert_24 } } @@ -162,6 +163,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "casper-network" -> R.drawable.img_casper_22 "xodex" -> R.drawable.img_xodex_22 "canxium" -> R.drawable.img_canxium_22 + "chiliz", "chiliz/test" -> R.drawable.img_chiliz_22 else -> R.drawable.ic_alert_24 } } @@ -245,6 +247,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "casper", "casper/test" -> R.drawable.ic_casper_22 "xodex" -> R.drawable.ic_xodex_22 "canxium" -> R.drawable.ic_canxium_22 + "chiliz", "chiliz/test" -> R.drawable.ic_chiliz_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_chiliz_22.xml b/core/ui/src/main/res/drawable/ic_chiliz_22.xml new file mode 100644 index 0000000000..8d1f446943 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_chiliz_22.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_chiliz_22.xml b/core/ui/src/main/res/drawable/img_chiliz_22.xml new file mode 100644 index 0000000000..70f61bbde0 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_chiliz_22.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index 5378b4b964..ba16c78bf0 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { // region Others dependencies implementation(deps.kotlin.coroutines) + implementation(deps.jodatime) implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.timber) diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 2a8c0ed1d7..25703406d5 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -5,6 +5,7 @@ import com.tangem.blockchainsdk.compatibility.applyL2Compatibility import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.getNetwork import com.tangem.data.common.utils.retryOnError @@ -13,8 +14,10 @@ import com.tangem.data.markets.converters.* import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi +import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesResponse import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi.Companion.marketsQuoteFields +import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository @@ -27,12 +30,15 @@ import kotlinx.coroutines.withContext import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong +@Suppress("LongParameterList") internal class DefaultMarketsTokenRepository( private val marketsApi: TangemTechMarketsApi, private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, private val dispatcherProvider: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, + private val cacheRegistry: CacheRegistry, + private val tokenExchangesStore: RuntimeStateStore>, ) : MarketsTokenRepository { private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher( @@ -266,9 +272,13 @@ internal class DefaultMarketsTokenRepository( override suspend fun getTokenExchanges(tokenId: String): List { return withContext(dispatcherProvider.io) { - val response = marketsApi.getCoinExchanges(coinId = tokenId).getOrThrow() + cacheRegistry.invokeOnExpire(key = "coins/$tokenId/exchanges", skipCache = false) { + val response = marketsApi.getCoinExchanges(coinId = tokenId).getOrThrow() - TokenMarketExchangeConverter.convertList(input = response.exchanges) + tokenExchangesStore.store(value = response.exchanges) + } + + TokenMarketExchangeConverter.convertList(input = tokenExchangesStore.get().value) } } diff --git a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt index fa4e3d492f..056e3c82f7 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt @@ -1,9 +1,11 @@ package com.tangem.data.markets.di import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.markets.DefaultMarketsTokenRepository import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -25,6 +27,7 @@ internal object MarketsDataModule { userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, analyticsEventHandler: AnalyticsEventHandler, + cacheRegistry: CacheRegistry, ): MarketsTokenRepository { return DefaultMarketsTokenRepository( marketsApi = marketsApi, @@ -32,6 +35,8 @@ internal object MarketsDataModule { dispatcherProvider = dispatchers, userWalletsStore = userWalletsStore, analyticsEventHandler = analyticsEventHandler, + cacheRegistry = cacheRegistry, + tokenExchangesStore = RuntimeStateStore(defaultValue = emptyList()), ) } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index bdbb147344..31afbf4c67 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -4,19 +4,29 @@ import com.tangem.data.onramp.converters.CountryConverter import com.tangem.data.onramp.converters.CurrencyConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.onramp.OnrampApi +import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO +import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext internal class DefaultOnrampRepository( private val onrampApi: OnrampApi, private val dispatchers: CoroutineDispatcherProvider, + private val appPreferencesStore: AppPreferencesStore, ) : OnrampRepository { private val currencyConverter = CurrencyConverter() - private val countryConverter = CountryConverter() + private val countryConverter = CountryConverter(currencyConverter) override suspend fun getCurrencies(): List = withContext(dispatchers.io) { onrampApi.getCurrencies() @@ -29,4 +39,48 @@ internal class DefaultOnrampRepository( .getOrThrow() .map(countryConverter::convert) } + + override suspend fun getCountryByIp(): OnrampCountry = withContext(dispatchers.io) { + onrampApi.getCountryByIp() + .getOrThrow() + .let(countryConverter::convert) + } + + override suspend fun saveDefaultCurrency(currency: OnrampCurrency) = withContext(dispatchers.io) { + appPreferencesStore.storeObject( + key = PreferencesKeys.ONRAMP_DEFAULT_CURRENCY, + value = currencyConverter.convertBack(currency), + ) + } + + override suspend fun getDefaultCurrencySync(): OnrampCurrency? = withContext(dispatchers.io) { + appPreferencesStore + .getObjectSyncOrNull(PreferencesKeys.ONRAMP_DEFAULT_CURRENCY) + ?.let(currencyConverter::convert) + } + + override fun getDefaultCurrency(): Flow { + return appPreferencesStore + .getObject(PreferencesKeys.ONRAMP_DEFAULT_CURRENCY) + .map { it?.let(currencyConverter::convert) } + } + + override suspend fun saveDefaultCountry(country: OnrampCountry) = withContext(dispatchers.io) { + appPreferencesStore.storeObject( + key = PreferencesKeys.ONRAMP_DEFAULT_COUNTRY, + value = countryConverter.convertBack(country), + ) + } + + override suspend fun getDefaultCountrySync(): OnrampCountry? = withContext(dispatchers.io) { + appPreferencesStore + .getObjectSyncOrNull(PreferencesKeys.ONRAMP_DEFAULT_COUNTRY) + ?.let(countryConverter::convert) + } + + override fun getDefaultCountry(): Flow { + return appPreferencesStore + .getObject(PreferencesKeys.ONRAMP_DEFAULT_COUNTRY) + .map { it?.let(countryConverter::convert) } + } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/CountryConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/CountryConverter.kt index 6d1c21f2d8..97099c62b1 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/CountryConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/CountryConverter.kt @@ -2,13 +2,14 @@ package com.tangem.data.onramp.converters import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO import com.tangem.domain.onramp.model.OnrampCountry -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter -internal class CountryConverter : Converter { - - private val currencyConverter = CurrencyConverter() +internal class CountryConverter( + private val currencyConverter: CurrencyConverter, +) : TwoWayConverter { override fun convert(value: OnrampCountryDTO) = OnrampCountry( + id = "${value.alpha3}-${value.name}", name = value.name, code = value.code, image = value.image, @@ -17,4 +18,14 @@ internal class CountryConverter : Converter { defaultCurrency = currencyConverter.convert(value.defaultCurrency), onrampAvailable = value.onrampAvailable, ) + + override fun convertBack(value: OnrampCountry): OnrampCountryDTO = OnrampCountryDTO( + name = value.name, + code = value.code, + image = value.image, + alpha3 = value.alpha3, + continent = value.continent, + defaultCurrency = currencyConverter.convertBack(value.defaultCurrency), + onrampAvailable = value.onrampAvailable, + ) } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/CurrencyConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/CurrencyConverter.kt index 01e8ee6309..4c37be2fc8 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/CurrencyConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/CurrencyConverter.kt @@ -2,9 +2,9 @@ package com.tangem.data.onramp.converters import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO import com.tangem.domain.onramp.model.OnrampCurrency -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter -internal class CurrencyConverter : Converter { +internal class CurrencyConverter : TwoWayConverter { override fun convert(value: OnrampCurrencyDTO) = OnrampCurrency( name = value.name, @@ -12,4 +12,11 @@ internal class CurrencyConverter : Converter image = value.image, precision = value.precision, ) + + override fun convertBack(value: OnrampCurrency): OnrampCurrencyDTO = OnrampCurrencyDTO( + name = value.name, + code = value.code, + image = value.image, + precision = value.precision, + ) } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index c1b0d24e4b..d47543851a 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -2,6 +2,7 @@ package com.tangem.data.onramp.di import com.tangem.data.onramp.DefaultOnrampRepository import com.tangem.datasource.api.onramp.OnrampApi +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -16,10 +17,15 @@ internal object OnrampDataModule { @Provides @Singleton - fun provideOnrampRepository(onrampApi: OnrampApi, dispatchers: CoroutineDispatcherProvider): OnrampRepository { + fun provideOnrampRepository( + onrampApi: OnrampApi, + dispatchers: CoroutineDispatcherProvider, + appPreferencesStore: AppPreferencesStore, + ): OnrampRepository { return DefaultOnrampRepository( onrampApi = onrampApi, dispatchers = dispatchers, + appPreferencesStore = appPreferencesStore, ) } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 466e1346ff..00a9d68849 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -48,7 +48,6 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero @@ -67,7 +66,6 @@ internal class DefaultStakingRepository( private val stakingBalanceStore: StakingBalanceStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, - private val stakingFeatureToggle: StakingFeatureToggles, private val walletManagersFacade: WalletManagersFacade, private val getUserWalletUseCase: GetUserWalletUseCase, moshi: Moshi, @@ -322,8 +320,6 @@ internal class DefaultStakingRepository( cryptoCurrency: CryptoCurrency, refresh: Boolean, ) = withContext(dispatchers.io) { - if (!stakingFeatureToggle.isStakingEnabled) return@withContext - cacheRegistry.invokeOnExpire( key = getYieldBalancesKey(userWalletId), skipCache = refresh, @@ -363,30 +359,26 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): Flow = channelFlow { - if (!stakingFeatureToggle.isStakingEnabled) { - send(YieldBalance.Empty) - } else { - launch(dispatchers.io) { - val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() - val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] - ?: error("Could not get integrationId") - stakingBalanceStore.get(userWalletId, address, integrationId) - .distinctUntilChanged() - .collectLatest { - if (it != null) { - send(yieldBalanceConverter.convert(it)) - } else { - error("No yield balance available for currency ${cryptoCurrency.id.value}") - } + launch(dispatchers.io) { + val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] + ?: error("Could not get integrationId") + stakingBalanceStore.get(userWalletId, address, integrationId) + .distinctUntilChanged() + .collectLatest { + if (it != null) { + send(yieldBalanceConverter.convert(it)) + } else { + error("No yield balance available for currency ${cryptoCurrency.id.value}") } - } + } + } - withContext(dispatchers.io) { - fetchSingleYieldBalance( - userWalletId, - cryptoCurrency, - ) - } + withContext(dispatchers.io) { + fetchSingleYieldBalance( + userWalletId, + cryptoCurrency, + ) } }.cancellable() @@ -394,21 +386,17 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): YieldBalance = withContext(dispatchers.io) { - if (!stakingFeatureToggle.isStakingEnabled) { - YieldBalance.Empty - } else { - fetchSingleYieldBalance(userWalletId, cryptoCurrency) + fetchSingleYieldBalance(userWalletId, cryptoCurrency) - val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() + val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() - val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] - ?: error("Could not get integrationId") + val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] + ?: error("Could not get integrationId") - val result = stakingBalanceStore.getSyncOrNull(userWalletId, address, integrationId) - ?: return@withContext YieldBalance.Error + val result = stakingBalanceStore.getSyncOrNull(userWalletId, address, integrationId) + ?: return@withContext YieldBalance.Error - yieldBalanceConverter.convert(result) - } + yieldBalanceConverter.convert(result) } override suspend fun fetchMultiYieldBalance( @@ -416,8 +404,6 @@ internal class DefaultStakingRepository( cryptoCurrencies: List, refresh: Boolean, ) = withContext(dispatchers.io) { - if (!stakingFeatureToggle.isStakingEnabled) return@withContext - cacheRegistry.invokeOnExpire( key = getYieldBalancesKey(userWalletId), skipCache = refresh, @@ -474,19 +460,15 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrencies: List, ): Flow = channelFlow { - if (!stakingFeatureToggle.isStakingEnabled) { - send(YieldBalanceList.Empty) - } else { - stakingBalanceStore.get(userWalletId) - .onEach { - val balances = yieldBalanceListConverter.convert(it) - send(balances) - } - .launchIn(scope = this + dispatchers.io) - - withContext(dispatchers.io) { - fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) + stakingBalanceStore.get(userWalletId) + .onEach { + val balances = yieldBalanceListConverter.convert(it) + send(balances) } + .launchIn(scope = this + dispatchers.io) + + withContext(dispatchers.io) { + fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) } } @@ -494,13 +476,9 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrencies: List, ): YieldBalanceList = withContext(dispatchers.io) { - if (!stakingFeatureToggle.isStakingEnabled) { - YieldBalanceList.Empty - } else { - fetchMultiYieldBalance(userWalletId, cryptoCurrencies) - val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error - yieldBalanceListConverter.convert(result) - } + fetchMultiYieldBalance(userWalletId, cryptoCurrencies) + val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error + yieldBalanceListConverter.convert(result) } override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean { diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index f3a7879d5a..336bcb651e 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -18,7 +18,6 @@ import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -38,7 +37,6 @@ internal object StakingDataModule { stakingBalanceStore: StakingBalanceStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, - stakingFeatureToggle: StakingFeatureToggles, walletManagersFacade: WalletManagersFacade, getUserWalletUseCase: GetUserWalletUseCase, @NetworkMoshi moshi: Moshi, @@ -49,7 +47,6 @@ internal object StakingDataModule { stakingBalanceStore = stakingBalanceStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, - stakingFeatureToggle = stakingFeatureToggle, walletManagersFacade = walletManagersFacade, getUserWalletUseCase = getUserWalletUseCase, moshi = moshi, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index cc7bb7a3fe..de57a5ed7c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -2,12 +2,11 @@ package com.tangem.data.tokens.di import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.repository.* -import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.quote.QuotesStore -import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade @@ -26,23 +25,21 @@ internal object TokensDataModule { @Singleton fun provideCurrenciesRepository( tangemTechApi: TangemTechApi, - tangemExpressApi: TangemExpressApi, appPreferencesStore: AppPreferencesStore, userWalletsStore: UserWalletsStore, walletManagersFacade: WalletManagersFacade, - expressAssetsStore: ExpressAssetsStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, + swapServiceLoader: SwapServiceLoader, ): CurrenciesRepository { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, - tangemExpressApi = tangemExpressApi, - appPreferencesStore = appPreferencesStore, - walletManagersFacade = walletManagersFacade, userWalletsStore = userWalletsStore, - expressAssetsStore = expressAssetsStore, + walletManagersFacade = walletManagersFacade, cacheRegistry = cacheRegistry, + appPreferencesStore = appPreferencesStore, dispatchers = dispatchers, + swapServiceLoader = swapServiceLoader, ) } @@ -84,15 +81,6 @@ internal object TokensDataModule { ) } - @Provides - @Singleton - fun provideDefaultMarketCoinsRepository( - expressAssetsStore: ExpressAssetsStore, - coroutineDispatcherProvider: CoroutineDispatcherProvider, - ): MarketCryptoCurrencyRepository { - return DefaultMarketCryptoCurrencyRepository(expressAssetsStore, coroutineDispatcherProvider) - } - @Provides @Singleton fun provideCurrencyChecksRepository( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index a739407505..31282f6f34 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -12,18 +12,14 @@ import com.tangem.data.tokens.utils.CustomTokensMerger import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.express.TangemExpressApi -import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE -import com.tangem.datasource.api.express.models.request.AssetsRequestBody -import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject -import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.DemoConfig @@ -46,12 +42,11 @@ import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, - private val tangemExpressApi: TangemExpressApi, private val userWalletsStore: UserWalletsStore, private val walletManagersFacade: WalletManagersFacade, - private val expressAssetsStore: ExpressAssetsStore, private val cacheRegistry: CacheRegistry, private val appPreferencesStore: AppPreferencesStore, + private val swapServiceLoader: SwapServiceLoader, private val dispatchers: CoroutineDispatcherProvider, ) : CurrenciesRepository { @@ -565,27 +560,7 @@ internal class DefaultCurrenciesRepository( userWalletId: UserWalletId, userTokens: UserTokensResponse, ) { - try { - val tokensList = userTokens.tokens - .map { - LeastTokenInfo( - contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, - network = it.networkId, - ) - } - - if (tokensList.isNotEmpty()) { - val response = tangemExpressApi.getAssets( - AssetsRequestBody( - tokensList = tokensList, - ), - ) - - expressAssetsStore.store(userWalletId, response.getOrThrow()) - } - } catch (e: Throwable) { - Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") - } + swapServiceLoader.update(userWalletId, userTokens) } private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt deleted file mode 100644 index 83f30079f3..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.data.tokens.repository - -import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE -import com.tangem.datasource.local.token.ExpressAssetsStore -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -class DefaultMarketCryptoCurrencyRepository( - private val expressAssetsStore: ExpressAssetsStore, - private val dispatchers: CoroutineDispatcherProvider, -) : MarketCryptoCurrencyRepository { - - override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { - return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom - } - - private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { - return withContext(dispatchers.io) { - val contractAddress = - (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE - - val asset = expressAssetsStore.getSyncOrNull(userWalletId)?.find { - it.network == cryptoCurrency.network.backendId && - it.contractAddress.equals(contractAddress, ignoreCase = true) - } - - asset?.exchangeAvailable ?: false - } - } -} \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index f01a2e6a96..ab9922a279 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(projects.common) implementation(projects.libs.auth) implementation(projects.libs.blockchainSdk) + implementation(projects.domain.core) implementation(projects.domain.demo) implementation(projects.domain.models) implementation(projects.domain.tokens.models) diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index 2536d78ea9..512be7ca64 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -1,7 +1,10 @@ package com.tangem.domain.exchange +import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow /** * Manager that holds info about available actions as Sell and Buy @@ -11,4 +14,16 @@ interface RampStateManager { fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean + + suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean + + suspend fun fetchBuyServiceData() + + fun getBuyInitializationStatus(): Flow> + + suspend fun fetchSellServiceData() + + fun getSellInitializationStatus(): Flow> + + fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow> } \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAvailability.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAvailability.kt new file mode 100644 index 0000000000..8113348d12 --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAvailability.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.onramp.model + +sealed interface OnrampAvailability { + + data object Available : OnrampAvailability + data class NotSupported(val country: OnrampCountry) : OnrampAvailability + data class ConfirmResidency(val country: OnrampCountry) : OnrampAvailability +} \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCountry.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCountry.kt index fb718b7cc1..a430172a2e 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCountry.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCountry.kt @@ -1,6 +1,10 @@ package com.tangem.domain.onramp.model +import kotlinx.serialization.Serializable + +@Serializable data class OnrampCountry( + val id: String, val name: String, val code: String, val image: String, diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrencies.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrencies.kt new file mode 100644 index 0000000000..1af69cd6ef --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrencies.kt @@ -0,0 +1,3 @@ +package com.tangem.domain.onramp.model + +data class OnrampCurrencies(val populars: List, val others: List) \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt index 7f8fd42c20..4f3d44f222 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt @@ -1,5 +1,8 @@ package com.tangem.domain.onramp.model +import kotlinx.serialization.Serializable + +@Serializable data class OnrampCurrency( val name: String, val code: String, diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt new file mode 100644 index 0000000000..c2aa371b07 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampAvailability +import com.tangem.domain.onramp.repositories.OnrampRepository + +class CheckOnrampAvailabilityUseCase(private val repository: OnrampRepository) { + + suspend operator fun invoke(): Either { + return Either.catch { + return@catch OnrampAvailability.Available + repository.getDefaultCountrySync()?.let { savedCountry -> + return@catch if (savedCountry.onrampAvailable) { + OnrampAvailability.Available + } else { + OnrampAvailability.NotSupported(savedCountry) + } + } + + val detectedCountry = repository.getCountryByIp() + OnrampAvailability.ConfirmResidency(detectedCountry) + } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCountriesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCountriesUseCase.kt new file mode 100644 index 0000000000..bbf1323710 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCountriesUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampCountry +import com.tangem.domain.onramp.repositories.OnrampRepository + +class GetOnrampCountriesUseCase(private val onrampRepository: OnrampRepository) { + + suspend operator fun invoke(): Either> { + return Either.catch { onrampRepository.getCountries() } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCountryUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCountryUseCase.kt new file mode 100644 index 0000000000..209324027a --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCountryUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.onramp.model.OnrampCountry +import com.tangem.domain.onramp.repositories.OnrampRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +class GetOnrampCountryUseCase(private val repository: OnrampRepository) { + + operator fun invoke(): Flow> { + return repository.getDefaultCountry() + .map> { it.right() } + .catch { emit(it.left()) } + } + + suspend fun invokeSync(): Either { + return Either.catch { repository.getDefaultCountrySync() ?: repository.getCountryByIp() } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrenciesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrenciesUseCase.kt new file mode 100644 index 0000000000..b1206947db --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrenciesUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampCurrencies +import com.tangem.domain.onramp.repositories.OnrampRepository + +class GetOnrampCurrenciesUseCase( + private val onrampRepository: OnrampRepository, +) { + + suspend operator fun invoke(): Either { + return Either.catch { + val currenciesList = onrampRepository.getCurrencies() + val (populars, others) = currenciesList.toSet() + .partition { popularFiatCodes.contains(it.code.uppercase()) } + OnrampCurrencies(populars = populars, others = others) + } + } + + private companion object { + val popularFiatCodes = setOf("USD", "EUR", "GBP", "CAD", "AUD", "HKD") + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveDefaultCountryUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveDefaultCountryUseCase.kt new file mode 100644 index 0000000000..e939cfee5f --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveDefaultCountryUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.onramp + +import com.tangem.domain.onramp.model.OnrampCountry +import com.tangem.domain.onramp.repositories.OnrampRepository + +class OnrampSaveDefaultCountryUseCase(private val repository: OnrampRepository) { + + suspend operator fun invoke(country: OnrampCountry) { + repository.saveDefaultCountry(country) + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveDefaultCurrencyUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveDefaultCurrencyUseCase.kt new file mode 100644 index 0000000000..6362609fbf --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveDefaultCurrencyUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.onramp + +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.onramp.repositories.OnrampRepository + +class OnrampSaveDefaultCurrencyUseCase(private val repository: OnrampRepository) { + + suspend operator fun invoke(currency: OnrampCurrency) { + repository.saveDefaultCurrency(currency) + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt index 8b251a9d47..85b4e27f69 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt @@ -2,8 +2,16 @@ package com.tangem.domain.onramp.repositories import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.domain.onramp.model.OnrampCurrency +import kotlinx.coroutines.flow.Flow interface OnrampRepository { suspend fun getCurrencies(): List suspend fun getCountries(): List + suspend fun getCountryByIp(): OnrampCountry + suspend fun saveDefaultCurrency(currency: OnrampCurrency) + suspend fun getDefaultCurrencySync(): OnrampCurrency? + fun getDefaultCurrency(): Flow + suspend fun saveDefaultCountry(country: OnrampCountry) + suspend fun getDefaultCountrySync(): OnrampCountry? + fun getDefaultCountry(): Flow } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index ad321616d3..2e91b110df 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -7,12 +7,10 @@ import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -27,12 +25,10 @@ import kotlinx.coroutines.flow.* class GetCryptoCurrencyActionsUseCase( private val rampManager: RampStateManager, private val walletManagersFacade: WalletManagersFacade, - private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, - private val stakingFeatureToggles: StakingFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -134,28 +130,26 @@ class GetCryptoCurrencyActionsUseCase( } // staking - if (stakingFeatureToggles.isStakingEnabled) { - if (isStakingAvailable(userWallet, cryptoCurrency)) { - val yield = kotlin.runCatching { - stakingRepository.getYield( - cryptoCurrencyId = cryptoCurrency.id, - symbol = cryptoCurrency.symbol, - ) - }.getOrNull() - activeList.add( - TokenActionsState.ActionState.Stake( - unavailabilityReason = ScenarioUnavailabilityReason.None, - yield = yield, - ), + if (isStakingAvailable(userWallet, cryptoCurrency)) { + val yield = kotlin.runCatching { + stakingRepository.getYield( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, ) - } else { - disabledList.add( - TokenActionsState.ActionState.Stake( - unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), - yield = null, - ), - ) - } + }.getOrNull() + activeList.add( + TokenActionsState.ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.None, + yield = yield, + ), + ) + } else { + disabledList.add( + TokenActionsState.ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), + yield = null, + ), + ) } // send @@ -172,7 +166,7 @@ class GetCryptoCurrencyActionsUseCase( // swap if (userWallet.isMultiCurrency) { if ( - marketCryptoCurrencyRepository.isExchangeable(userWallet.walletId, cryptoCurrency) && + rampManager.availableForSwap(userWallet.walletId, cryptoCurrency) && cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote ) { activeList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None)) @@ -271,9 +265,7 @@ class GetCryptoCurrencyActionsUseCase( } actionsList.add(TokenActionsState.ActionState.Receive(scenario)) } - if (stakingFeatureToggles.isStakingEnabled) { - actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null)) - } + actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null)) actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) return actionsList diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index fc77aa776b..9118e3098d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* @@ -26,7 +27,7 @@ class GetCurrencyWarningsUseCase( private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val swapRepository: SwapRepository, - private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, + private val rampStateManager: RampStateManager, private val stakingRepository: StakingRepository, private val promoRepository: PromoRepository, private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, @@ -90,10 +91,10 @@ class GetCurrencyWarningsUseCase( val cryptoStatuses = operations.getCurrenciesStatusesSync() val promoBanner = promoRepository.getOkxPromoBanner() return combine( - showSwapPromoTokenUseCase() + flow = showSwapPromoTokenUseCase() .conflate() .distinctUntilChanged(), - flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)) + flow2 = flowOf(rampStateManager.availableForSwap(userWalletId, currency)) .conflate() .distinctUntilChanged(), ) { shouldShowSwapPromo, isExchangeable -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt deleted file mode 100644 index 0e0d223a53..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.domain.tokens.repository - -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.models.UserWalletId - -/** - * MarketCryptoCurrencyRepository works with data from Tangem coins backend, CoinMarketCap etc - */ -interface MarketCryptoCurrencyRepository { - - suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt index 3d92875a5b..67080c0682 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt @@ -38,6 +38,8 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV } } + fun getDefaultFallbackUrl(): String = Artwork.DEFAULT_IMG_URL + private fun getFallbackArtworkUrl(cardId: String): String { return when { cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL @@ -45,7 +47,7 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV else -> when (TwinsHelper.getTwinCardNumber(cardId)) { TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL - else -> Artwork.DEFAULT_IMG_URL + else -> getDefaultFallbackUrl() } } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index 6b4e801849..3a6cc45745 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -62,9 +62,9 @@ internal class OnboardingManageTokensModel @Inject constructor( .launchIn(modelScope) combine( - manageTokensListManager.currenciesToAdd, - manageTokensListManager.currenciesToRemove, - ::handleChangedCurrencies, + flow = manageTokensListManager.currenciesToAdd, + flow2 = manageTokensListManager.currenciesToRemove, + transform = ::handleChangedCurrencies, ).launchIn(modelScope) observeSearchQueryChanges() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 11ae6093f5..9d7e300646 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -531,11 +531,11 @@ internal class MarketsTokenDetailsModel @Inject constructor( showBottomSheet(content = ExchangesBottomSheetContent.Loading(exchangesCount)) - // Delay to show the bottom sheet - delay(timeMillis = 800L) - val maybeExchanges = getTokenExchangesUseCase(tokenId = params.token.id) + // Delay to show the bottom sheet + delay(timeMillis = 400L) + updateExchangeBSContent(maybeExchanges = maybeExchanges, exchangesCount = exchangesCount) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt index f6f4d3ba8a..0ddafb4e83 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.audits.AuditLabelUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.markets.TokenMarketExchange import com.tangem.domain.markets.TokenMarketExchange.TrustScore @@ -26,7 +27,7 @@ internal object ExchangeItemStateConverter : Converter onTokenItemClick(status) }, ) return PortfolioTokenUM( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 8d7bc12e6c..2333f6b48f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -149,7 +149,7 @@ internal class TokenActionsHandler @AssistedInject constructor( private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { router.push( AppRoute.Swap( - currency = cryptoCurrencyData.status.currency, + currencyFrom = cryptoCurrencyData.status.currency, userWalletId = cryptoCurrencyData.userWallet.walletId, isInitialReverseOrder = true, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt index e084f9e6eb..7b1d5695df 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt @@ -32,7 +32,7 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie val onClick = state.tokenItemState.onItemClick if (onClick != null) { hapticManager.perform(TangemHapticEffect.View.ContextClick) - onClick.invoke() + onClick.invoke(it) } }, ) @@ -81,8 +81,8 @@ private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM: PortfolioItem( state = tokenUM.copy( tokenItemState = when (tokenUM.tokenItemState) { - is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = onItemClick) - is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = onItemClick) + is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() }) + is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() }) else -> tokenUM.tokenItemState }, isQuickActionsShown = quickActionsShown, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index b2728ffc33..79177f24a4 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM @@ -49,10 +50,12 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 25f02a083b..fac172d9d1 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -28,6 +28,8 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.feedback) implementation(projects.domain.core) + implementation(projects.domain.card) + implementation(projects.domain.wallets) /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) @@ -54,6 +56,7 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) implementation(deps.timber) + implementation(deps.firebase.crashlytics) /** DI */ implementation(deps.hilt.android) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt index ac06b7c4a5..623f315db0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt @@ -105,6 +105,14 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor( }.saveIn(innerNavigationLinkJobHolder) } } + + componentScope.launch { + model.titleProvider.currentTitle.collect { title -> + stepperComponent.state.update { + it.copy(title = title) + } + } + } } @Composable diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index 9a040ff17c..07053a7b9e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -40,12 +40,12 @@ internal class OnboardingEntryModel @Inject constructor( return when (scanResponse.productType) { ProductType.Note -> TODO() ProductType.Twins -> TODO() - ProductType.Wallet -> OnboardingRoute.Wallet12( + ProductType.Wallet -> OnboardingRoute.MultiWallet( scanResponse = scanResponse, withSeedPhraseFlow = false, titleProvider = titleProvider, ) - ProductType.Wallet2 -> OnboardingRoute.Wallet12( + ProductType.Wallet2 -> OnboardingRoute.MultiWallet( scanResponse = scanResponse, withSeedPhraseFlow = true, titleProvider = titleProvider, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt index 3cd1b811a8..e25190780d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt @@ -5,12 +5,12 @@ import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletCo import javax.inject.Inject internal class OnboardingChildFactory @Inject constructor( - private val wallet12ComponentFactory: OnboardingMultiWalletComponent.Factory, + private val multiWalletComponentFactory: OnboardingMultiWalletComponent.Factory, ) { fun createChild(route: OnboardingRoute, childContext: AppComponentContext): Any { return when (route) { - is OnboardingRoute.Wallet12 -> wallet12ComponentFactory.create( + is OnboardingRoute.MultiWallet -> multiWalletComponentFactory.create( context = childContext, params = OnboardingMultiWalletComponent.Params( scanResponse = route.scanResponse, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt index 4dbd6593f0..b315398e7c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt @@ -8,7 +8,7 @@ sealed class OnboardingRoute : Route { data object None : OnboardingRoute() - data class Wallet12( + data class MultiWallet( val titleProvider: TitleProvider, val scanResponse: ScanResponse, val withSeedPhraseFlow: Boolean, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt index 16400dd642..37d5e6acd7 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt @@ -31,7 +31,7 @@ internal inline fun OnboardingEntry( animation = stackAnimation(slide()), ) { when (it.configuration) { - is OnboardingRoute.Wallet12 -> { + is OnboardingRoute.MultiWallet -> { (it.instance as OnboardingMultiWalletComponent).Content( modifier = modifier, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt index 8cc7a9e553..846e1f0d53 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt @@ -4,12 +4,24 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.stack.* +import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerNavigation import com.tangem.core.decompose.navigation.inner.InnerNavigationState import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams +import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.MultiWalletCreateWalletComponent import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel +import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState import com.tangem.features.onboarding.v2.multiwallet.impl.ui.OnboardingMultiWallet import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -22,24 +34,82 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor ) : OnboardingMultiWalletComponent, AppComponentContext by context { private val model: OnboardingMultiWalletModel = getOrCreateModel(params) + private val stackNavigation = StackNavigation() override val innerNavigation: InnerNavigation = object : InnerNavigation { override val state = MutableStateFlow( - Wallet12InnerNavigationState(1, 5), // TODO + MultiWalletInnerNavigationState(1, 5), ) override fun pop(onComplete: (Boolean) -> Unit) { - // TODO + // TODO add warning dialog + stackNavigation.pop(onComplete) + } + } + + private val childStack: Value> = childStack( + key = "innerStack", + source = stackNavigation, + serializer = null, + initialConfiguration = model.state.value.currentStep, + handleBackButton = true, + childFactory = { configuration, factoryContext -> createChild(configuration, childByContext(factoryContext)) }, + ) + + private fun createChild( + step: OnboardingMultiWalletState.Step, + childContext: AppComponentContext, + ): MultiWalletChildComponent { + return when (step) { + OnboardingMultiWalletState.Step.CreateWallet -> MultiWalletCreateWalletComponent( + context = childContext, + params = MultiWalletChildParams( + multiWalletState = model.state, + parentParams = params, + ), + onDone = ::onStepDone, + ) + OnboardingMultiWalletState.Step.AddBackupDevice -> MultiWalletBackupComponent( + context = childContext, + params = MultiWalletChildParams( + multiWalletState = model.state, + parentParams = params, + ), + onDone = ::onStepDone, + ) + OnboardingMultiWalletState.Step.FinishBackup -> TODO() + OnboardingMultiWalletState.Step.Done -> TODO() + } + } + + private fun onStepDone() { + when (model.state.value.currentStep) { + OnboardingMultiWalletState.Step.CreateWallet -> { + stackNavigation.push(OnboardingMultiWalletState.Step.AddBackupDevice) + // innerNavigation.state.value TODO + } + OnboardingMultiWalletState.Step.AddBackupDevice -> TODO() + OnboardingMultiWalletState.Step.FinishBackup -> TODO() + OnboardingMultiWalletState.Step.Done -> TODO() } } @Composable override fun Content(modifier: Modifier) { - val uiState by model.uiState.collectAsStateWithLifecycle() + val stackState by childStack.subscribeAsState() + val state by model.uiState.collectAsStateWithLifecycle() OnboardingMultiWallet( + state = state, modifier = modifier, - state = uiState, + childContent = { mdfr -> + Children( + stack = stackState, + animation = stackAnimation(slide()), + ) { + it.instance.Content(mdfr) + } + }, ) } @@ -52,7 +122,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor } } -data class Wallet12InnerNavigationState( +data class MultiWalletInnerNavigationState( override val stackSize: Int, override val stackMaxSize: Int?, ) : InnerNavigationState \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/MultiWalletChildComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/MultiWalletChildComponent.kt new file mode 100644 index 0000000000..c484f28a70 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/MultiWalletChildComponent.kt @@ -0,0 +1,5 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child + +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface MultiWalletChildComponent : ComposableContentComponent \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/MultiWalletChildParams.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/MultiWalletChildParams.kt new file mode 100644 index 0000000000..3e2d3bffb3 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/MultiWalletChildParams.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child + +import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState +import kotlinx.coroutines.flow.MutableStateFlow + +class MultiWalletChildParams( + val multiWalletState: MutableStateFlow, + val parentParams: OnboardingMultiWalletComponent.Params, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/MultiWalletBackupComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/MultiWalletBackupComponent.kt new file mode 100644 index 0000000000..f68f4741b3 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/MultiWalletBackupComponent.kt @@ -0,0 +1,45 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model.MultiWalletBackupModel +import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.MultiWalletBackup +import kotlinx.coroutines.launch + +@Suppress("UnusedPrivateMember") +class MultiWalletBackupComponent( + context: AppComponentContext, + params: MultiWalletChildParams, + onDone: () -> Unit, +) : AppComponentContext by context, MultiWalletChildComponent { + + private val model: MultiWalletBackupModel = getOrCreateModel(params) + + init { + params.parentParams.titleProvider.changeTitle( + text = resourceReference(R.string.onboarding_navbar_title_creating_backup), + ) + + componentScope.launch { + model.onDoneFlow.collect { onDone() } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + MultiWalletBackup( + modifier = modifier, + state = state, + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/BackupState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/BackupState.kt new file mode 100644 index 0000000000..b1fd1a646c --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/BackupState.kt @@ -0,0 +1,48 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model + +import android.graphics.Bitmap +import com.tangem.common.card.Card + +// TODO maybe delete some fields in the future +data class BackupState( + val primaryCardId: String? = null, + val primaryCardBatchId: String? = null, + val backupCardsNumber: Int = 0, + val backupCards: List = emptyList(), + val backupCardIds: List = emptyList(), + val backupCardBatchIds: List = emptyList(), + @Transient + val backupCardsArtworks: Map = emptyMap(), + val accessCode: String = "", + val accessCodeError: AccessCodeError? = null, + val backupStep: BackupStep? = null, + val maxBackupCards: Int = 2, + val canSkipBackup: Boolean = true, + val isInterruptedBackup: Boolean = false, + val showBtnLoading: Boolean = false, + val hasBackupError: Boolean = false, + val startedSource: BackupStartedSource = BackupStartedSource.Onboarding, + val hasRing: Boolean = false, +) + +enum class BackupStartedSource { + Onboarding, CreateBackup +} + +enum class AccessCodeError { + CodeTooShort, CodesDoNotMatch +} + +typealias CardId = String + +sealed class BackupStep { + data object InitBackup : BackupStep() + data object ScanOriginCard : BackupStep() + data object AddBackupCards : BackupStep() + data object SetAccessCode : BackupStep() + data object EnterAccessCode : BackupStep() + data object ReenterAccessCode : BackupStep() + data object WritePrimaryCard : BackupStep() + data class WriteBackupCard(val cardNumber: Int) : BackupStep() + data object Finished : BackupStep() +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt new file mode 100644 index 0000000000..66ffa8ff7f --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt @@ -0,0 +1,201 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemSdkError +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.scan.ProductType +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams +import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.backupCardAttestationFailedDialog +import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.resetBackupCardDialog +import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state.MultiWalletBackupUM +import com.tangem.operations.backup.BackupService +import com.tangem.sdk.api.BackupServiceHolder +import com.tangem.sdk.api.TangemSdkManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ComponentScoped +class MultiWalletBackupModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val backupServiceHolder: BackupServiceHolder, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val tangemSdkManager: TangemSdkManager, +) : Model() { + + @Suppress("UnusedPrivateMember") + private val params = paramsContainer.require() + private val scanResponse + get() = params.multiWalletState.value.currentScanResponse + private val backupService + get() = backupServiceHolder.backupService.get()!! + + private val state = MutableStateFlow(BackupState()) + + private val _uiState = MutableStateFlow( + when (scanResponse.productType) { + ProductType.Wallet -> getWallet1State() + ProductType.Wallet2, + ProductType.Ring, + -> MultiWalletBackupUM.Wallet2( + isRing = scanResponse.productType == ProductType.Ring, + backupAdded = false, // backupService.hasIncompletedBackup, + onAddBackupClick = {}, + onFinalizeButtonClick = {}, + ) + else -> error("Type: ${scanResponse.productType.name} is not supported!") + }, + ) + + val uiState: StateFlow = _uiState + val onDoneFlow = MutableSharedFlow() + + private fun getWallet1State(): MultiWalletBackupUM.Wallet1 { + return when (backupService.currentState) { + BackupService.State.Preparing -> { + MultiWalletBackupUM.Wallet1( + title = resourceReference(R.string.onboarding_title_no_backup_cards), + bodyText = resourceReference(R.string.onboarding_subtitle_no_backup_cards), + showFinalizeButton = false, + addBackupButtonEnabled = true, + addBackupButtonLoading = false, + artworkNumberOfBackupCards = 0, + onAddBackupClick = ::startBackupWallet1, + onFinalizeButtonClick = {}, + onSkipButtonClick = { onDoneFlow.tryEmit(Unit) }, + ) + } + is BackupService.State.FinalizingBackupCard -> TODO() + BackupService.State.FinalizingPrimaryCard -> TODO() + BackupService.State.Finished -> TODO() + } + } + + private fun startBackupWallet1() { + backupService.discardSavedBackup() + val primaryCard = scanResponse.primaryCard + + if (primaryCard != null) { + backupService.setPrimaryCard(primaryCard) + addBackupCardWithService() + } else { + // TODO wallet 2 ??? or from main? + // BackupAction.StartAddingPrimaryCard + } + } + + private fun setNumberOfBackupCardsWallet1(number: Int) { + // set state for Wallet1 for adding backup cards and disable button if there is more than 2 backup cards + _uiState.update { st -> + if (st !is MultiWalletBackupUM.Wallet1) return@update st + + when (number) { + 0 -> { + st.copy( + title = resourceReference(R.string.onboarding_title_no_backup_cards), + bodyText = resourceReference(R.string.onboarding_subtitle_no_backup_cards), + artworkNumberOfBackupCards = 0, + addBackupButtonEnabled = true, + ) + } + 1 -> { + st.copy( + title = resourceReference(R.string.onboarding_title_one_backup_card), + bodyText = resourceReference(R.string.onboarding_subtitle_one_backup_card), + artworkNumberOfBackupCards = 1, + addBackupButtonEnabled = true, + showFinalizeButton = true, + ) + } + 2 -> { + st.copy( + title = resourceReference(R.string.onboarding_title_two_backup_cards), + bodyText = resourceReference(R.string.onboarding_subtitle_two_backup_cards), + artworkNumberOfBackupCards = 2, + addBackupButtonEnabled = false, + ) + } + else -> st.copy(addBackupButtonEnabled = false) + } + } + } + + private fun addBackupCardWithService() { + updateWallet1State { st -> + st.copy(addBackupButtonLoading = true) + } + + backupService.addBackupCard { result -> + backupService.skipCompatibilityChecks = false + cardSdkConfigRepository.sdk.config.filter.cardIdFilter = null + + updateWallet1State { st -> + st.copy(addBackupButtonLoading = false) + } + + when (result) { + is CompletionResult.Success -> { + // TODO change artwork + state.update { + it.copy( + backupCards = it.backupCards + result.data, + backupCardsNumber = it.backupCardsNumber + 1, + ) + } + setNumberOfBackupCardsWallet1(state.value.backupCardsNumber) + } + is CompletionResult.Failure -> { + when (val error = result.error) { + is TangemSdkError.BackupFailedNotEmptyWallets -> { + updateWallet1State { st -> + st.copy( + dialog = resetBackupCardDialog( + onReset = { resetBackupCard(cardId = error.cardId) }, + onDismiss = { updateWallet1State { it.copy(dialog = null) } }, + ), + ) + } + } + is TangemSdkError.IssuerSignatureLoadingFailed -> { + updateWallet1State { st -> + st.copy( + dialog = backupCardAttestationFailedDialog( + onDismiss = { updateWallet1State { it.copy(dialog = null) } }, + ), + ) + } + } + else -> FirebaseCrashlytics.getInstance().recordException(result.error) + } + } + } + } + } + + private fun resetBackupCard(cardId: String) { + modelScope.launch { + tangemSdkManager.resetToFactorySettings( + cardId = cardId, + allowsRequestAccessCodeFromRepository = false, + ) + } + } + + private fun updateWallet1State(block: (MultiWalletBackupUM.Wallet1) -> MultiWalletBackupUM.Wallet1) { + _uiState.update { state -> + if (state !is MultiWalletBackupUM.Wallet1) return@update state + block(state) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/Dialogs.kt new file mode 100644 index 0000000000..5b9acbb42e --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/Dialogs.kt @@ -0,0 +1,27 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state.MultiWalletBackupUMDialog + +internal fun resetBackupCardDialog(onReset: () -> Unit, onDismiss: () -> Unit) = MultiWalletBackupUMDialog( + title = resourceReference(R.string.common_attention), + bodyText = resourceReference(R.string.onboarding_linking_error_card_with_wallets), + confirmText = resourceReference(R.string.common_cancel), + cancelText = resourceReference(R.string.card_settings_action_sheet_reset), + warningCancelColor = true, + onDismiss = onDismiss, + onConfirm = onDismiss, + onCancel = { onReset(); onDismiss() }, +) + +internal fun backupCardAttestationFailedDialog(onDismiss: () -> Unit) = MultiWalletBackupUMDialog( + title = resourceReference(R.string.common_error), + bodyText = resourceReference(R.string.issuer_signature_loading_failed), + confirmText = resourceReference(R.string.common_ok), + cancelText = null, + warningCancelColor = true, + onDismiss = onDismiss, + onConfirm = onDismiss, + onCancel = null, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/MultiWalletBackup.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/MultiWalletBackup.kt new file mode 100644 index 0000000000..a8a1e22522 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/MultiWalletBackup.kt @@ -0,0 +1,151 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.* +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state.MultiWalletBackupUM + +@Composable +fun MultiWalletBackup(state: MultiWalletBackupUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + when (state) { + is MultiWalletBackupUM.Wallet1 -> Wallet1Content(state) + is MultiWalletBackupUM.Wallet2 -> TODO() + } + } + + val dialog = state.dialog + if (dialog != null) { + BasicDialog( + title = dialog.title.resolveReference(), + message = dialog.bodyText.resolveReference(), + confirmButton = DialogButtonUM( + title = dialog.confirmText.resolveReference(), + onClick = dialog.onConfirm, + ), + dismissButton = if (dialog.cancelText != null && dialog.onCancel != null) { + DialogButtonUM( + title = dialog.cancelText.resolveReference(), + warning = dialog.warningCancelColor, + onClick = dialog.onCancel, + ) + } else { + null + }, + onDismissDialog = dialog.onDismiss, + ) + } +} + +@Composable +private fun ColumnScope.Wallet1Content(state: MultiWalletBackupUM.Wallet1) { + Column( + Modifier + .fillMaxWidth() + .padding(start = 32.dp, end = 32.dp, bottom = 74.dp) + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // artwork + + Text( + text = state.title.resolveReference(), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 16.dp), + ) + + Text( + text = state.bodyText.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 12.dp), + ) + } + + if (state.showFinalizeButton) { + SecondaryButtonIconEnd( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .fillMaxWidth(), + iconResId = R.drawable.ic_tangem_24, + enabled = state.addBackupButtonEnabled, + showProgress = state.addBackupButtonLoading, + text = stringResource(R.string.onboarding_button_add_backup_card), + onClick = state.onAddBackupClick, + ) + + PrimaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResource(R.string.onboarding_button_finalize_backup), + onClick = state.onFinalizeButtonClick, + ) + } else { + PrimaryButtonIconEnd( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .fillMaxWidth(), + iconResId = R.drawable.ic_tangem_24, + enabled = state.addBackupButtonEnabled, + showProgress = state.addBackupButtonLoading, + text = stringResource(R.string.onboarding_button_add_backup_card), + onClick = state.onAddBackupClick, + ) + + SecondaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResource(R.string.onboarding_button_skip_backup), + onClick = state.onSkipButtonClick, + ) + } +} + +@Preview +@Composable +private fun PreviewWallet1() { + TangemThemePreview { + MultiWalletBackup( + state = MultiWalletBackupUM.Wallet1( + title = stringReference("Title"), + bodyText = stringReference("Body body body"), + ), + ) + } +} + +@Preview +@Composable +private fun PreviewWallet2() { + TangemThemePreview { + MultiWalletBackup( + state = MultiWalletBackupUM.Wallet2( + isRing = false, + backupAdded = true, + onAddBackupClick = {}, + onFinalizeButtonClick = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/state/MultiWalletBackupUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/state/MultiWalletBackupUM.kt new file mode 100644 index 0000000000..cb3351c3db --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/ui/state/MultiWalletBackupUM.kt @@ -0,0 +1,45 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state + +import androidx.annotation.IntRange +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference + +@Immutable +sealed class MultiWalletBackupUM { + + abstract val dialog: MultiWalletBackupUMDialog? + + data class Wallet1( + val title: TextReference = stringReference(""), + val bodyText: TextReference = stringReference(""), + val addBackupButtonEnabled: Boolean = true, + val addBackupButtonLoading: Boolean = false, + val showFinalizeButton: Boolean = false, + val oneBackupAdded: Boolean = false, + @IntRange(0, 2) val artworkNumberOfBackupCards: Int = 0, // highlights cards on 0, 1, 2 + val onAddBackupClick: () -> Unit = {}, + val onFinalizeButtonClick: () -> Unit = {}, + val onSkipButtonClick: () -> Unit = {}, + override val dialog: MultiWalletBackupUMDialog? = null, + ) : MultiWalletBackupUM() + + data class Wallet2( + val isRing: Boolean, + val backupAdded: Boolean, + val onAddBackupClick: () -> Unit, + val onFinalizeButtonClick: () -> Unit, + override val dialog: MultiWalletBackupUMDialog? = null, + ) : MultiWalletBackupUM() +} + +data class MultiWalletBackupUMDialog( + val title: TextReference, + val bodyText: TextReference, + val confirmText: TextReference, + val cancelText: TextReference?, + val warningCancelColor: Boolean = false, + val onConfirm: () -> Unit, + val onCancel: (() -> Unit)?, + val onDismiss: () -> Unit, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/MultiWalletCreateWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/MultiWalletCreateWalletComponent.kt new file mode 100644 index 0000000000..713eb54a50 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/MultiWalletCreateWalletComponent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model.MultiWalletCreateWalletModel +import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.MultiWalletCreateWallet +import kotlinx.coroutines.launch + +class MultiWalletCreateWalletComponent( + context: AppComponentContext, + params: MultiWalletChildParams, + onDone: () -> Unit, +) : AppComponentContext by context, MultiWalletChildComponent { + + private val model: MultiWalletCreateWalletModel = getOrCreateModel(params) + + init { + params.parentParams.titleProvider.changeTitle( + text = resourceReference(R.string.onboarding_create_wallet_header), + ) + + componentScope.launch { + model.onDone.collect { onDone() } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + MultiWalletCreateWallet( + modifier = modifier, + state = state, + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/Dialogs.kt new file mode 100644 index 0000000000..d3b790586d --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/Dialogs.kt @@ -0,0 +1,23 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM + +internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismissButtonClick: () -> Unit) = + MultiWalletCreateWalletUM.Dialog( + title = resourceReference(R.string.onboarding_activation_error_title), + description = resourceReference(R.string.onboarding_activation_error_message), + confirmButtonText = resourceReference(R.string.common_support), + dismissButtonText = resourceReference(R.string.common_ok), + dismissWarningColor = true, + onDismiss = dismiss, + onConfirm = { + onConfirm() + dismiss() + }, + onDismissButtonClick = { + onDismissButtonClick() + dismiss() + }, + ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt new file mode 100644 index 0000000000..e32cbde7d2 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -0,0 +1,106 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model + +import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemSdkError +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams +import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM +import com.tangem.sdk.api.TangemSdkManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ComponentScoped +class MultiWalletCreateWalletModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val tangemSdkManager: TangemSdkManager, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val cardRepository: CardRepository, +) : Model() { + + private val params = paramsContainer.require() + private val multiWalletState + get() = params.multiWalletState + + private val _uiState = MutableStateFlow( + MultiWalletCreateWalletUM( + onCreateWalletClick = { createWallet(false) }, + showSeedPhraseOption = params.parentParams.withSeedPhraseFlow, + onOtherOptionsClick = { /* navigate */ }, + dialog = null, + ), + ) + + val uiState: StateFlow = _uiState + val onDone = MutableSharedFlow() + + private fun createWallet(shouldReset: Boolean) { + modelScope.launch { + val result = tangemSdkManager.createProductWallet( + scanResponse = multiWalletState.value.currentScanResponse, + shouldReset = shouldReset, + ) + + when (result) { + is CompletionResult.Success -> { + multiWalletState.update { + it.copy( + currentScanResponse = it.currentScanResponse.copy( + card = result.data.card, + derivedKeys = result.data.derivedKeys, + primaryCard = result.data.primaryCard, + ), + ) + } + + cardRepository.startCardActivation(cardId = result.data.card.cardId) + onDone.emit(Unit) + // TODO + // Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) + } + + is CompletionResult.Failure -> { + if (result.error is TangemSdkError.WalletAlreadyCreated) { + // show should reset dialog + handleActivationError() + } + } + } + } + } + + private fun handleActivationError() { + _uiState.update { state -> + state.copy( + dialog = resetCardDialog( + onConfirm = ::navigateToSupportScreen, + dismiss = { _uiState.update { it.copy(dialog = null) } }, + onDismissButtonClick = ::resetCard, + ), + ) + } + } + + private fun resetCard() { + createWallet(true) + } + + fun navigateToSupportScreen() { + modelScope.launch { + val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch + sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/MultiWalletCreateWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/MultiWalletCreateWallet.kt new file mode 100644 index 0000000000..b20e13c3af --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/MultiWalletCreateWallet.kt @@ -0,0 +1,75 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM + +@Composable +fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier: Modifier = Modifier) { + if (state.dialog != null) { + BasicDialog( + title = state.dialog.title.resolveReference(), + message = state.dialog.description.resolveReference(), + confirmButton = DialogButtonUM( + title = state.dialog.confirmButtonText.resolveReference(), + onClick = state.dialog.onConfirm, + ), + dismissButton = DialogButtonUM( + title = state.dialog.dismissButtonText.resolveReference(), + warning = state.dialog.dismissWarningColor, + onClick = state.dialog.onDismissButtonClick, + ), + onDismissDialog = state.dialog.onDismiss, + ) + } + + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + verticalArrangement = Arrangement.Bottom, + ) { + PrimaryButtonIconEnd( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .fillMaxWidth(), + iconResId = R.drawable.ic_tangem_24, + text = stringResource(R.string.onboarding_create_wallet_button_create_wallet), + onClick = state.onCreateWalletClick, + ) + + SecondaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResource(R.string.onboarding_create_wallet_options_button_options), + onClick = state.onOtherOptionsClick, + ) + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + MultiWalletCreateWallet( + state = MultiWalletCreateWalletUM( + onCreateWalletClick = {}, + showSeedPhraseOption = true, + onOtherOptionsClick = {}, + dialog = null, + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt new file mode 100644 index 0000000000..c761e1f481 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt @@ -0,0 +1,21 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state + +import com.tangem.core.ui.extensions.TextReference + +data class MultiWalletCreateWalletUM( + val onCreateWalletClick: () -> Unit, + val showSeedPhraseOption: Boolean, + val onOtherOptionsClick: () -> Unit, + val dialog: Dialog?, +) { + data class Dialog( + val title: TextReference, + val description: TextReference, + val dismissButtonText: TextReference, + val confirmButtonText: TextReference, + val dismissWarningColor: Boolean = false, + val onConfirm: () -> Unit, + val onDismissButtonClick: () -> Unit, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/di/ComponentModule.kt index 1c7fd39182..363655f367 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/di/ComponentModule.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/di/ComponentModule.kt @@ -4,6 +4,8 @@ import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.decompose.model.Model import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent import com.tangem.features.onboarding.v2.multiwallet.impl.DefaultOnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model.MultiWalletBackupModel +import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.model.MultiWalletCreateWalletModel import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel import dagger.Binds import dagger.Module @@ -30,4 +32,14 @@ internal interface ModelModule { @IntoMap @ClassKey(OnboardingMultiWalletModel::class) fun provideModel(model: OnboardingMultiWalletModel): Model + + @Binds + @IntoMap + @ClassKey(MultiWalletBackupModel::class) + fun provideModel2(model: MultiWalletBackupModel): Model + + @Binds + @IntoMap + @ClassKey(MultiWalletCreateWalletModel::class) + fun provideModel3(model: MultiWalletCreateWalletModel): Model } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Dialogs.kt deleted file mode 100644 index f6905984f6..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Dialogs.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.features.onboarding.v2.multiwallet.impl.model - -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM - -internal fun resetCardDialog(onConfirm: () -> Unit, onDismiss: () -> Unit, onDismissButtonClick: () -> Unit) = - OnboardingMultiWalletUM.Dialog( - title = resourceReference(R.string.onboarding_activation_error_title), - description = resourceReference(R.string.onboarding_activation_error_message), - confirmButtonText = resourceReference(R.string.common_ok), - dismissButtonText = resourceReference(R.string.common_support), - onDismiss = onDismiss, - onConfirm = onConfirm, - onDismissButtonClick = onDismissButtonClick, - ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt index f031b2a3f6..5a8c2f5fca 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -1,22 +1,14 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.model -import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemSdkError import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.feedback.GetCardInfoUseCase -import com.tangem.domain.feedback.SendFeedbackEmailUseCase -import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM -import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -24,36 +16,31 @@ import javax.inject.Inject internal class OnboardingMultiWalletModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val tangemSdkManager: TangemSdkManager, - private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val getCardInfoUseCase: GetCardInfoUseCase, ) : Model() { - private val params = paramsContainer.require() - private val currentScanResponse = MutableStateFlow(params.scanResponse) + private val getCardImageUseCase = GetCardImageUseCase() + private val _uiState = MutableStateFlow(OnboardingMultiWalletUM()) val state = MutableStateFlow( OnboardingMultiWalletState( currentStep = getInitialStep(), + currentScanResponse = params.scanResponse, ), ) - val uiState = MutableStateFlow( - OnboardingMultiWalletUM( - onCreateWalletClick = { createWallet(false) }, - showSeedPhraseOption = params.withSeedPhraseFlow, - onOtherOptionsClick = { /* navigate */ }, - onBack = { }, - dialog = null, - ), - ) + val uiState = _uiState.asStateFlow() init { + // TODO add analytics + // if (!manager.isActivationStarted(notNullCard.cardId)) { + // Analytics.send(Onboarding.Started()) + // } initScreenTitleSub() + loadCardArtwork() } private fun getInitialStep(): OnboardingMultiWalletState.Step { - val card = currentScanResponse.value.card + val card = params.scanResponse.card return when { card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> @@ -70,71 +57,24 @@ internal class OnboardingMultiWalletModel @Inject constructor( params.titleProvider.changeTitle(title) modelScope.launch { - state - .map { it.currentStep } + state.map { it.currentStep } .collectLatest { step -> - val title = screenTitleByStep(step) - params.titleProvider.changeTitle(title) + val stepTitle = screenTitleByStep(step) + params.titleProvider.changeTitle(stepTitle) } } } - private fun createWallet(shouldReset: Boolean) { + private fun loadCardArtwork() { modelScope.launch { - val result = tangemSdkManager.createProductWallet( - scanResponse = currentScanResponse.value, - shouldReset = shouldReset, - ) + val artwork = + getCardImageUseCase.invoke(params.scanResponse.card.cardId, params.scanResponse.card.cardPublicKey) - when (result) { - is CompletionResult.Success -> { - currentScanResponse.update { - it.copy( - card = result.data.card, - derivedKeys = result.data.derivedKeys, - primaryCard = result.data.primaryCard, - ) - } - - // TODO - // Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) - } - - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.WalletAlreadyCreated) { - // show should reset dialog - handleActivationError() - } + if (artwork != getCardImageUseCase.getDefaultFallbackUrl()) { + _uiState.update { + it.copy(artworkUrl = artwork) } } } } - - private fun handleActivationError() { - uiState.update { - it.copy( - dialog = resetCardDialog( - onConfirm = { - uiState.update { it.copy(dialog = null) } - resetCard() - }, - onDismiss = { - uiState.update { it.copy(dialog = null) } - }, - onDismissButtonClick = ::navigateToSupportScreen, - ), - ) - } - } - - private fun resetCard() { - createWallet(true) - } - - fun navigateToSupportScreen() { - modelScope.launch { - val cardInfo = getCardInfoUseCase(currentScanResponse.value).getOrNull() ?: return@launch - sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) - } - } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletState.kt index d1b11653dd..0c3ecc83ea 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletState.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletState.kt @@ -1,10 +1,12 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.model +import com.tangem.domain.models.scan.ScanResponse + data class OnboardingMultiWalletState( val currentStep: Step, + val currentScanResponse: ScanResponse, ) { - enum class Step { - GeneratePrivateKeys, CreateWallet, AddBackupDevice, FinishBackup, Done + CreateWallet, AddBackupDevice, FinishBackup, Done } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Utils.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Utils.kt index fb735811de..73573ab2a6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Utils.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Utils.kt @@ -5,9 +5,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.onboarding.v2.impl.R fun screenTitleByStep(step: OnboardingMultiWalletState.Step): TextReference = when (step) { - OnboardingMultiWalletState.Step.GeneratePrivateKeys -> TODO() - OnboardingMultiWalletState.Step.CreateWallet -> resourceReference(R.string.onboarding_create_wallet_header) - OnboardingMultiWalletState.Step.AddBackupDevice -> TODO() + OnboardingMultiWalletState.Step.CreateWallet -> + resourceReference(R.string.onboarding_create_wallet_header) + OnboardingMultiWalletState.Step.AddBackupDevice -> + resourceReference(R.string.onboarding_navbar_title_creating_backup) OnboardingMultiWalletState.Step.FinishBackup -> TODO() OnboardingMultiWalletState.Step.Done -> TODO() } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt index d2de011033..9a3b0efcec 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt @@ -1,79 +1,63 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.ui +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM @Composable -internal fun OnboardingMultiWallet(state: OnboardingMultiWalletUM, modifier: Modifier = Modifier) { - if (state.dialog != null) { - BasicDialog( - title = state.dialog.title.resolveReference(), - message = state.dialog.description.resolveReference(), - confirmButton = DialogButtonUM( - title = state.dialog.confirmButtonText.resolveReference(), - onClick = state.dialog.onConfirm, - ), - dismissButton = DialogButtonUM( - title = state.dialog.dismissButtonText.resolveReference(), - onClick = state.dialog.onDismissButtonClick, - ), - onDismissDialog = state.dialog.onDismiss, - ) - } - +@Suppress("MagicNumber") +internal fun OnboardingMultiWallet( + state: OnboardingMultiWalletUM, + modifier: Modifier = Modifier, + childContent: @Composable (Modifier) -> Unit = {}, +) { Column( modifier = modifier .fillMaxSize() - .navigationBarsPadding() .verticalScroll(rememberScrollState()), ) { - Box(Modifier.weight(1f)) - - PrimaryButtonIconEnd( + WalletArtworks( + url = state.artworkUrl, modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .weight(.56f) + .padding(horizontal = 34.dp) + .padding(top = 20.dp) .fillMaxWidth(), - iconResId = R.drawable.ic_tangem_24, - text = stringResource(R.string.onboarding_create_wallet_button_create_wallet), - onClick = state.onCreateWalletClick, + state = WalletArtworksState.Folded, ) - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResource(R.string.onboarding_create_wallet_options_button_options), - onClick = state.onOtherOptionsClick, - ) + Box( + modifier = Modifier.weight(.44f), + contentAlignment = Alignment.BottomStart, + ) { + childContent(Modifier) + } } } -@Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 640) @Composable private fun Preview() { TangemThemePreview { OnboardingMultiWallet( - state = OnboardingMultiWalletUM( - onCreateWalletClick = {}, - showSeedPhraseOption = true, - onBack = {}, - onOtherOptionsClick = {}, - dialog = null, - ), + state = OnboardingMultiWalletUM(artworkUrl = null), + childContent = { md -> + Box( + modifier = md + .fillMaxWidth() + .height(200.dp) + .background(Color.DarkGray), + ) + }, ) } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/WalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/WalletArtwork.kt new file mode 100644 index 0000000000..fa5c1643a7 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/WalletArtwork.kt @@ -0,0 +1,462 @@ +@file:Suppress("MagicNumber", "UnnecessaryParentheses", "CyclomaticComplexMethod", "LongMethod") +package com.tangem.features.onboarding.v2.multiwallet.impl.ui + +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.zIndex +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import kotlinx.coroutines.delay +import java.util.concurrent.TimeUnit + +sealed class WalletArtworksState { + data object Folded : WalletArtworksState() + + data object Fan : WalletArtworksState() + + data class Stack( + val threeCards: Boolean = true, + ) : WalletArtworksState() + + data class Unfold( + val step: Step = Step.First, + ) : WalletArtworksState() { + enum class Step { + First, Second, Third + } + } + + data class Leapfrog( + val threeCards: Boolean = true, + val step: Step = Step.Second, + ) : WalletArtworksState() { + enum class Step { + Second, Third + } + } +} + +@Composable +fun WalletArtworks(url: String?, state: WalletArtworksState, modifier: Modifier = Modifier) { + BoxWithConstraints( + modifier + .heightIn(min = 180.dp) + .widthIn(max = 450.dp), + ) { + val animationWidth = maxWidth + val density = LocalDensity.current.density + + var animationState by remember { + mutableStateOf( + state.toTransitionSetState( + maxWidthDp = animationWidth.value, + density = density, + maxHeightDp = maxHeight.value, + ).first(), + ) + } + + val transition1 = updateTransition(animationState.walletCard1, label = "transition") + val transition2 = updateTransition(animationState.walletCard2, label = "transition") + val transition3 = updateTransition(animationState.walletCard3, label = "transition") + + LaunchedEffect(state) { + val animationSetState = state.toTransitionSetState( + maxWidthDp = animationWidth.value, + density = density, + maxHeightDp = maxHeight.value, + ) + animationSetState.fastForEach { + animationState = it + val maxTime = maxOf( + transition1.totalDurationNanos, + transition2.totalDurationNanos, + transition3.totalDurationNanos, + ) + delay(TimeUnit.NANOSECONDS.toMillis(maxTime)) + } + } + + AnimatedWalletCards( + url = url, + modifier = Modifier + .widthIn(max = 450.dp) + .matchParentSize(), + transition1 = transition1, + transition2 = transition2, + transition3 = transition3, + ) + } +} + +private data class CardsTransitionState( + val walletCard1: WalletCardTransitionState, + val walletCard2: WalletCardTransitionState, + val walletCard3: WalletCardTransitionState, +) + +private data class WalletCardTransitionState( + val alpha: Float = 1f, + val xTranslation: Float = 0f, + val yTranslation: Float = 0f, + val xScale: Float = 1f, + val yScale: Float = 1f, + val rotation: Float = 0f, + val zIndex: Float = 0f, +) + +@Composable +private fun AnimatedWalletCards( + url: String?, + transition1: Transition, + transition2: Transition, + transition3: Transition, + modifier: Modifier = Modifier, +) { + val alpha1 by transition1.animateFloat(label = "alpha1") { it.alpha } + val translationX1 by transition1.animateFloat(label = "translationX1") { it.xTranslation } + val translationY1 by transition1.animateFloat(label = "translationY1") { it.yTranslation } + val rotation1 by transition1.animateFloat(label = "rotation1") { it.rotation } + val scaleX1 by transition1.animateFloat(label = "scaleX1") { it.xScale } + val scaleY1 by transition1.animateFloat(label = "scaleY1") { it.yScale } + val zIndex1 = transition1.currentState.zIndex + + val alpha2 by transition2.animateFloat(label = "alpha2") { it.alpha } + val translationX2 by transition2.animateFloat(label = "translationX2") { it.xTranslation } + val translationY2 by transition2.animateFloat(label = "translationY2") { it.yTranslation } + val rotation2 by transition2.animateFloat(label = "rotation2") { it.rotation } + val scaleX2 by transition2.animateFloat(label = "scaleX2") { it.xScale } + val scaleY2 by transition2.animateFloat(label = "scaleY2") { it.yScale } + val zIndex2 = transition2.currentState.zIndex + + val alpha3 by transition3.animateFloat(label = "alpha3") { it.alpha } + val translationX3 by transition3.animateFloat(label = "translationX3") { it.xTranslation } + val translationY3 by transition3.animateFloat(label = "translationY3") { it.yTranslation } + val rotation3 by transition3.animateFloat(label = "rotation3") { it.rotation } + val scaleX3 by transition3.animateFloat(label = "scaleX3") { it.xScale } + val scaleY3 by transition3.animateFloat(label = "scaleY3") { it.yScale } + val zIndex3 = transition3.targetState.zIndex + + Box(modifier) { + WalletCard( + modifier = Modifier + .zIndex(zIndex3) + .matchParentSize() + .graphicsLayer { + translationX = translationX3 + translationY = translationY3 + } + .graphicsLayer { + alpha = alpha3 + scaleX = scaleX3 + scaleY = scaleY3 + rotationZ = rotation3 + }, + url = url, + ) + + WalletCard( + modifier = Modifier + .zIndex(zIndex2) + .matchParentSize() + .graphicsLayer { + translationX = translationX2 + translationY = translationY2 + } + .graphicsLayer { + alpha = alpha2 + rotationZ = rotation2 + + scaleX = scaleX2 + scaleY = scaleY2 + }, + url = url, + ) + + WalletCard( + modifier = Modifier + .zIndex(zIndex1) + .matchParentSize() + .graphicsLayer { + scaleX = scaleX1 + scaleY = scaleY1 + } + .graphicsLayer { + alpha = alpha1 + translationX = translationX1 + translationY = translationY1 + rotationZ = rotation1 + }, + url = url, + ) + } +} + +@Composable +private fun WalletCard(url: String?, modifier: Modifier = Modifier) { + AsyncImage( + modifier = modifier, + model = ImageRequest.Builder(LocalContext.current) + .data(url) + .crossfade(true) + .build(), + placeholder = painterResource(R.drawable.card_placeholder_black), + error = painterResource(R.drawable.card_placeholder_black), + fallback = painterResource(R.drawable.card_placeholder_black), + contentDescription = null, + ) +} + +private fun WalletArtworksState.toTransitionSetState(maxWidthDp: Float, maxHeightDp: Float, density: Float) = + when (this) { + is WalletArtworksState.Folded -> listOf( + CardsTransitionState( + WalletCardTransitionState(), + WalletCardTransitionState(), + WalletCardTransitionState(), + ), + ) + is WalletArtworksState.Stack -> listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState(), + walletCard2 = WalletCardTransitionState(), + walletCard3 = WalletCardTransitionState(), + ), + CardsTransitionState( + walletCard1 = WalletCardTransitionState().copy( + alpha = 1f, + xTranslation = 0f, + yTranslation = 0f, + rotation = 0f, + xScale = 1f, + yScale = 1f, + ), + walletCard2 = WalletCardTransitionState().copy( + alpha = 0.65f, + xTranslation = 0f, + yTranslation = ((maxHeightDp * (1 - 0.83f)) / 4 + 12) * density, + rotation = 0f, + xScale = 0.83f, + yScale = 0.83f, + ), + walletCard3 = WalletCardTransitionState().copy( + alpha = if (threeCards) 0.55f else 0f, + xTranslation = 0f, + yTranslation = (maxHeightDp * (1 - 0.72f) / 4 + 24) * density, + rotation = 0f, + xScale = 0.72f, + yScale = 0.72f, + ), + ), + ) + is WalletArtworksState.Unfold -> listOfNotNull( + CardsTransitionState( + walletCard1 = WalletCardTransitionState(), + walletCard2 = WalletCardTransitionState().copy( + alpha = 0.65f, + ), + walletCard3 = WalletCardTransitionState().copy( + alpha = 0.55f, + ), + ).takeIf { step == WalletArtworksState.Unfold.Step.First }, + CardsTransitionState( + walletCard1 = WalletCardTransitionState().copy( + rotation = 5f, + ), + walletCard2 = WalletCardTransitionState().copy( + alpha = when (step) { + WalletArtworksState.Unfold.Step.First -> 0.65f + WalletArtworksState.Unfold.Step.Second -> 1f + WalletArtworksState.Unfold.Step.Third -> 1f + }, + rotation = -6f, + xTranslation = 0f, + yTranslation = -(maxHeightDp * (1 - 0.88f) / 4) * density, + xScale = 0.88f, + yScale = 0.88f, + ), + walletCard3 = WalletCardTransitionState().copy( + alpha = when (step) { + WalletArtworksState.Unfold.Step.First -> 0.55f + WalletArtworksState.Unfold.Step.Second -> 0.55f + WalletArtworksState.Unfold.Step.Third -> 1f + }, + rotation = -16f, + xTranslation = 0f, + yTranslation = -(maxHeightDp * (1 - 0.81f) / 4 + 2) * density, + xScale = 0.81f, + yScale = 0.81f, + ), + ), + ) + WalletArtworksState.Fan -> { + listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState().copy( + xTranslation = maxWidthDp * 0.4f * density, + yTranslation = 35 * density, + rotation = 75f, + xScale = .85f, + yScale = .85f, + ), + walletCard2 = WalletCardTransitionState().copy( + xTranslation = maxWidthDp * -0.4f * density, + yTranslation = -5 * density, + rotation = 75f, + xScale = .85f, + yScale = .85f, + ), + walletCard3 = WalletCardTransitionState().copy( + rotation = 105f, + xScale = .85f, + yScale = .85f, + ), + ), + ) + } + is WalletArtworksState.Leapfrog -> { + val translationY = -(maxHeightDp / 2) * density + val secondCardPlace = WalletCardTransitionState().copy( + alpha = 0.65f, + xTranslation = 0f, + yTranslation = ((maxHeightDp * (1 - 0.83f)) / 4 + 12) * density, + rotation = 0f, + xScale = 0.83f, + yScale = 0.83f, + ) + val thirdCardPlace = WalletCardTransitionState().copy( + alpha = if (threeCards) 0.55f else 0f, + xTranslation = 0f, + yTranslation = (maxHeightDp * (1 - 0.72f) / 4 + 24) * density, + rotation = 0f, + xScale = 0.72f, + yScale = 0.72f, + ) + + if (step == WalletArtworksState.Leapfrog.Step.Second) { + listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState(), + walletCard2 = secondCardPlace, + walletCard3 = thirdCardPlace, + ), + CardsTransitionState( + walletCard1 = WalletCardTransitionState().copy( + yTranslation = translationY, + ), + walletCard2 = WalletCardTransitionState().copy( + alpha = 1f, + zIndex = 4f, + ), + walletCard3 = thirdCardPlace, + ), + CardsTransitionState( + walletCard1 = WalletCardTransitionState().copy( + zIndex = 1f, + ), + walletCard2 = WalletCardTransitionState().copy( + zIndex = 4f, + ), + walletCard3 = secondCardPlace, + ), + ) + } else { + listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState().copy( + zIndex = 2f, + ), + walletCard2 = WalletCardTransitionState().copy( + zIndex = 3f, + ), + walletCard3 = secondCardPlace, + ), + CardsTransitionState( + walletCard1 = WalletCardTransitionState().copy( + yTranslation = translationY, + ), + walletCard2 = WalletCardTransitionState().copy( + zIndex = 3f, + yTranslation = translationY, + ), + walletCard3 = WalletCardTransitionState().copy( + alpha = 1f, + zIndex = 4f, + ), + ), + CardsTransitionState( + walletCard1 = WalletCardTransitionState().copy( + zIndex = 1f, + ), + walletCard2 = WalletCardTransitionState().copy( + zIndex = 4f, + ), + walletCard3 = WalletCardTransitionState().copy( + zIndex = 5f, + ), + ), + ) + } + } + } + +@Preview(showBackground = true, widthDp = 360, heightDp = 640) +@Composable +private fun Preview() { + TangemThemePreview { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + var state: WalletArtworksState by remember { mutableStateOf(WalletArtworksState.Folded) } + + WalletArtworks( + url = null, + state = state, + modifier = Modifier.fillMaxWidth(), + ) + + var index by remember { mutableIntStateOf(0) } + + Button( + modifier = Modifier + .zIndex(5f) + .align(Alignment.TopStart), + onClick = { + val list = listOf( + WalletArtworksState.Folded, + WalletArtworksState.Stack(), + WalletArtworksState.Fan, + WalletArtworksState.Unfold(step = WalletArtworksState.Unfold.Step.First), + WalletArtworksState.Unfold(step = WalletArtworksState.Unfold.Step.Second), + WalletArtworksState.Unfold(step = WalletArtworksState.Unfold.Step.Third), + WalletArtworksState.Stack(), + WalletArtworksState.Leapfrog(step = WalletArtworksState.Leapfrog.Step.Second), + WalletArtworksState.Leapfrog(step = WalletArtworksState.Leapfrog.Step.Third), + ) + + state = list[index % list.size] + index++ + }, + ) { + Text("Animate") + } + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt index 8ae99c8da0..1e4884ea7a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt @@ -1,21 +1,5 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state -import com.tangem.core.ui.extensions.TextReference - internal data class OnboardingMultiWalletUM( - val onCreateWalletClick: () -> Unit, - val showSeedPhraseOption: Boolean, - val onOtherOptionsClick: () -> Unit, - val onBack: () -> Unit, - val dialog: Dialog?, -) { - data class Dialog( - val title: TextReference, - val description: TextReference, - val dismissButtonText: TextReference, - val confirmButtonText: TextReference, - val onConfirm: () -> Unit, - val onDismissButtonClick: () -> Unit, - val onDismiss: () -> Unit, - ) -} \ No newline at end of file + val artworkUrl: String? = null, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt index 2f4253b9f6..0689f4cb86 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt @@ -17,6 +17,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch +import timber.log.Timber internal class DefaultOnboardingStepperComponent @AssistedInject constructor( @Assisted val context: AppComponentContext, @@ -27,6 +28,14 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor( override val state = instanceKeeper.getOrCreateSimple { MutableStateFlow(params.initState) } + init { + componentScope.launch { + state.collect { + Timber.tag("ASDASD").d("State: $it") + } + } + } + private fun openSupport() { componentScope.launch { val cardInfo = getCardInfoUseCase(params.scanResponse).getOrNull() ?: return@launch @@ -36,10 +45,10 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { - val state by state.collectAsStateWithLifecycle() + val uiState by state.collectAsStateWithLifecycle() OnboardingStepper( - state = state, + state = uiState, onBackClick = remember(this) { params.popBack }, onSupportButtonClick = remember(this) { ::openSupport }, modifier = modifier, diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt index 16ac6b24f7..219536ddb7 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent interface OnrampComponent : ComposableContentComponent { - class Params + data class Params(val currency: String) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/SwapSelectTokensComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/SwapSelectTokensComponent.kt new file mode 100644 index 0000000000..2e28ee5552 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/SwapSelectTokensComponent.kt @@ -0,0 +1,22 @@ +package com.tangem.features.onramp.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Swap select tokens component + * +[REDACTED_AUTHOR] + */ +interface SwapSelectTokensComponent : ComposableContentComponent { + + interface Factory : ComponentFactory + + /** + * Params + * + * @property userWalletId user wallet id + */ + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 1ff3b4c39c..314c0f9529 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -14,6 +14,9 @@ android { dependencies { /** Project - API */ implementation(projects.features.onramp.api) + implementation(projects.features.swap.domain) + implementation(projects.features.swap.domain.api) + implementation(projects.features.swap.domain.models) /** Project - Core */ implementation(projects.core.decompose) @@ -21,6 +24,7 @@ dependencies { implementation(projects.core.featuretoggles) /** Project - Common */ + implementation(projects.common.routing) implementation(projects.common.ui) /** Project - Domain */ @@ -30,6 +34,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.onramp) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampFeatureModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureModule.kt similarity index 76% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampFeatureModule.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureModule.kt index cac4df29b9..6e668e4f69 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampFeatureModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureModule.kt @@ -1,8 +1,6 @@ -package com.tangem.features.onramp.di +package com.tangem.features.onramp import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.features.onramp.DefaultOnrampFeatureToggles -import com.tangem.features.onramp.OnrampFeatureToggles import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/DefaultBuyCryptoComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/DefaultBuyCryptoComponent.kt index 8e36bf09b0..7654c3c5b1 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/DefaultBuyCryptoComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/DefaultBuyCryptoComponent.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext import com.tangem.features.onramp.component.BuyCryptoComponent -import com.tangem.features.onramp.entity.OnrampOperation import com.tangem.features.onramp.selecttoken.OnrampOperationComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -20,7 +19,7 @@ internal class DefaultBuyCryptoComponent @AssistedInject constructor( private val selectTokenComponent: OnrampOperationComponent = onrampOperationComponentFactory.create( context = appComponentContext, - params = OnrampOperationComponent.Params(operation = OnrampOperation.BUY, userWalletId = params.userWalletId), + params = OnrampOperationComponent.Params.Buy(userWalletId = params.userWalletId), ) @Composable diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultOnrampComponent.kt deleted file mode 100644 index 7ec0fb692a..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultOnrampComponent.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.tangem.features.onramp.component.impl - -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState -import com.arkivanov.decompose.router.slot.childSlot -import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.features.onramp.component.OnrampComponent -import com.tangem.features.onramp.component.ConfirmResidencyComponent -import com.tangem.features.onramp.entity.OnrampBottomSheetConfig -import com.tangem.features.onramp.model.OnrampModel -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class DefaultOnrampComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: OnrampComponent.Params, - private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, -) : OnrampComponent, AppComponentContext by context { - - private val model: OnrampModel = getOrCreateModel(params) - private val confirmResidencyBottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = null, - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - - @Composable - override fun Content(modifier: Modifier) { - val bottomSheet by confirmResidencyBottomSheetSlot.subscribeAsState() - BackHandler(onBack = router::pop) - - bottomSheet.child?.instance?.BottomSheet() - } - - private fun bottomSheetChild( - config: OnrampBottomSheetConfig, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = when (config) { - OnrampBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create( - context = childByContext(componentContext), - params = ConfirmResidencyComponent.Params( - countryName = "United States", - isOnrampSupported = true, - countryFlagUrl = "https://hatscripts.github.io/circle-flags/flags/us.svg", - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - } - - @AssistedFactory - interface Factory : OnrampComponent.Factory { - override fun create(context: AppComponentContext, params: OnrampComponent.Params): DefaultOnrampComponent - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/ConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ConfirmResidencyComponent.kt similarity index 56% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/ConfirmResidencyComponent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ConfirmResidencyComponent.kt index 74742aa575..dd8bd0a162 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/ConfirmResidencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ConfirmResidencyComponent.kt @@ -1,16 +1,12 @@ -package com.tangem.features.onramp.component +package com.tangem.features.onramp.confirmresidency import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.onramp.model.OnrampCountry internal interface ConfirmResidencyComponent : ComposableBottomSheetComponent { - data class Params( - val countryName: String, - val isOnrampSupported: Boolean, - val countryFlagUrl: String, - val onDismiss: () -> Unit, - ) + data class Params(val country: OnrampCountry, val onDismiss: (OnrampCountry) -> Unit) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt new file mode 100644 index 0000000000..25e5bce171 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt @@ -0,0 +1,82 @@ +package com.tangem.features.onramp.confirmresidency + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.onramp.confirmresidency.model.ConfirmResidencyModel +import com.tangem.features.onramp.confirmresidency.ui.ConfirmResidencyBottomSheet +import com.tangem.features.onramp.confirmresidency.ui.ConfirmResidencyBottomSheetContent +import com.tangem.features.onramp.entity.ConfirmResidencyBottomSheetConfig +import com.tangem.features.onramp.selectcountry.SelectCountryComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultConfirmResidencyComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: ConfirmResidencyComponent.Params, + private val selectCountryComponentFactory: SelectCountryComponent.Factory, +) : ConfirmResidencyComponent, AppComponentContext by context { + + private val model: ConfirmResidencyModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + + override fun dismiss() { + router.pop() + } + + @Composable + override fun BottomSheet() { + val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() + val bottomSheetConfig = remember(key1 = this) { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + ConfirmResidencyBottomSheet( + config = bottomSheetConfig, + content = { modifier -> + ConfirmResidencyBottomSheetContent(model = state, modifier = modifier) + }, + ) + + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: ConfirmResidencyBottomSheetConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + ConfirmResidencyBottomSheetConfig.SelectCountry -> selectCountryComponentFactory.create( + context = childByContext(componentContext), + params = SelectCountryComponent.Params(onDismiss = model.bottomSheetNavigation::dismiss), + ) + } + + @AssistedFactory + interface Factory : ConfirmResidencyComponent.Factory { + override fun create( + context: AppComponentContext, + params: ConfirmResidencyComponent.Params, + ): DefaultConfirmResidencyComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/preview/PreviewConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/PreviewConfirmResidencyComponent.kt similarity index 70% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/preview/PreviewConfirmResidencyComponent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/PreviewConfirmResidencyComponent.kt index b5e167f157..adc039dca9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/preview/PreviewConfirmResidencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/PreviewConfirmResidencyComponent.kt @@ -1,15 +1,15 @@ -package com.tangem.features.onramp.component.preview +package com.tangem.features.onramp.confirmresidency import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.onramp.component.ConfirmResidencyComponent -import com.tangem.features.onramp.entity.ConfirmResidencyUM -import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheet -import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheetContent +import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM +import com.tangem.features.onramp.confirmresidency.ui.ConfirmResidencyBottomSheet +import com.tangem.features.onramp.confirmresidency.ui.ConfirmResidencyBottomSheetContent import kotlinx.coroutines.flow.MutableStateFlow internal class PreviewConfirmResidencyComponent( @@ -31,11 +31,13 @@ internal class PreviewConfirmResidencyComponent( @Composable override fun BottomSheet() { val state by previewState.collectAsStateWithLifecycle() - val bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, - onDismissRequest = ::dismiss, - content = TangemBottomSheetConfigContent.Empty, - ) + val bottomSheetConfig = remember(key1 = this) { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } ConfirmResidencyBottomSheet( config = bottomSheetConfig, content = { modifier -> diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/di/OnrampConfirmResidencyComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/di/OnrampConfirmResidencyComponentModule.kt new file mode 100644 index 0000000000..7b97f25196 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/di/OnrampConfirmResidencyComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.confirmresidency.di + +import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent +import com.tangem.features.onramp.confirmresidency.DefaultConfirmResidencyComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampConfirmResidencyComponentModule { + + @Binds + @Singleton + fun bindConfirmResidencyComponentFactory( + factory: DefaultConfirmResidencyComponent.Factory, + ): ConfirmResidencyComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/di/OnrampConfirmResidencyModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/di/OnrampConfirmResidencyModelModule.kt new file mode 100644 index 0000000000..ab79a46e1c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/di/OnrampConfirmResidencyModelModule.kt @@ -0,0 +1,19 @@ +package com.tangem.features.onramp.confirmresidency.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.confirmresidency.model.ConfirmResidencyModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampConfirmResidencyModelModule { + @Binds + @IntoMap + @ClassKey(ConfirmResidencyModel::class) + fun bindConfirmResidencyModel(model: ConfirmResidencyModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyUM.kt similarity index 86% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyUM.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyUM.kt index 106df7e1bc..364743a457 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.entity +package com.tangem.features.onramp.confirmresidency.entity import com.tangem.core.ui.extensions.TextReference diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt new file mode 100644 index 0000000000..2b0f91f3d1 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt @@ -0,0 +1,70 @@ +package com.tangem.features.onramp.confirmresidency.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.onramp.GetOnrampCountryUseCase +import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent +import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.entity.ConfirmResidencyBottomSheetConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +@ComponentScoped +internal class ConfirmResidencyModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + getOnrampCountryUseCase: GetOnrampCountryUseCase, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: ConfirmResidencyComponent.Params = paramsContainer.require() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val state: MutableStateFlow = MutableStateFlow( + ConfirmResidencyUM( + country = params.country.name, + countryFlagUrl = params.country.image, + isCountrySupported = params.country.onrampAvailable, + primaryButtonConfig = getPrimaryButtonConfig(), + secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig( + onClick = ::onChangeClick, + text = resourceReference(R.string.common_change), + ), + ), + ) + + init { + getOnrampCountryUseCase.invoke() + .onEach { maybeCountry -> + val country = maybeCountry.getOrNull() + if (country != null) { + params.onDismiss(country) + } + } + .launchIn(modelScope) + } + + private fun getPrimaryButtonConfig() = if (params.country.onrampAvailable) { + ConfirmResidencyUM.ActionButtonConfig( + onClick = { params.onDismiss(params.country) }, + text = resourceReference(R.string.common_confirm), + ) + } else { + ConfirmResidencyUM.ActionButtonConfig( + onClick = router::pop, + text = resourceReference(R.string.common_close), + ) + } + + private fun onChangeClick() { + bottomSheetNavigation.activate(ConfirmResidencyBottomSheetConfig.SelectCountry) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/ui/ResidenceBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt similarity index 95% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/ui/ResidenceBottomSheet.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt index a950da23ab..cb95070456 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/ui/ResidenceBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.ui +package com.tangem.features.onramp.confirmresidency.ui import android.content.res.Configuration import androidx.compose.foundation.layout.* @@ -27,9 +27,9 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.onramp.component.ConfirmResidencyComponent -import com.tangem.features.onramp.component.preview.PreviewConfirmResidencyComponent -import com.tangem.features.onramp.entity.ConfirmResidencyUM +import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent +import com.tangem.features.onramp.confirmresidency.PreviewConfirmResidencyComponent +import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM import com.tangem.features.onramp.impl.R @Composable diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampComponentModule.kt deleted file mode 100644 index 6f27d391db..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampComponentModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.features.onramp.di - -import com.tangem.features.onramp.component.ConfirmResidencyComponent -import com.tangem.features.onramp.component.OnrampComponent -import com.tangem.features.onramp.component.impl.DefaultConfirmResidencyComponent -import com.tangem.features.onramp.component.impl.DefaultOnrampComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface OnrampComponentModule { - - @Binds - @Singleton - fun bindOnrampComponentFactory(factory: DefaultOnrampComponent.Factory): OnrampComponent.Factory - - @Binds - @Singleton - fun bindConfirmResidencyComponentFactory( - factory: DefaultConfirmResidencyComponent.Factory, - ): ConfirmResidencyComponent.Factory -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampModelModule.kt deleted file mode 100644 index 41f90f6d5a..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampModelModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.features.onramp.di - -import com.tangem.core.decompose.di.DecomposeComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.onramp.model.OnrampModel -import com.tangem.features.onramp.model.ConfirmResidencyModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(DecomposeComponent::class) -internal interface OnrampModelModule { - - @Binds - @IntoMap - @ClassKey(OnrampModel::class) - fun provideOnrampModel(model: OnrampModel): Model - - @Binds - @IntoMap - @ClassKey(ConfirmResidencyModel::class) - fun provideConfirmResidencyModel(model: ConfirmResidencyModel): Model -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyBottomSheetConfig.kt new file mode 100644 index 0000000000..cccafaaa8d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyBottomSheetConfig.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.entity + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class ConfirmResidencyBottomSheetConfig { + + @Serializable + data object SelectCountry : ConfirmResidencyBottomSheetConfig() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampBottomSheetConfig.kt deleted file mode 100644 index 81289b4cdf..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampBottomSheetConfig.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.onramp.entity - -import kotlinx.serialization.Serializable - -@Serializable -internal sealed class OnrampBottomSheetConfig { - - @Serializable - data object ConfirmResidency : OnrampBottomSheetConfig() -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt new file mode 100644 index 0000000000..be1d4dcb81 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt @@ -0,0 +1,66 @@ +package com.tangem.features.onramp.main + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent +import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig +import com.tangem.features.onramp.main.model.OnrampMainComponentModel +import com.tangem.features.onramp.main.ui.OnrampMainComponentContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnrampMainComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: OnrampMainComponent.Params, + private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, +) : OnrampMainComponent, AppComponentContext by appComponentContext { + + private val model: OnrampMainComponentModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsState() + val bottomSheet by bottomSheetSlot.subscribeAsState() + + OnrampMainComponentContent(modifier = modifier, state = state) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: OnrampMainBottomSheetConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + is OnrampMainBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create( + context = childByContext(componentContext), + params = ConfirmResidencyComponent.Params( + country = config.country, + onDismiss = { model.bottomSheetNavigation.dismiss() }, + ), + ) + } + + @AssistedFactory + interface Factory : OnrampMainComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnrampMainComponent.Params, + ): DefaultOnrampMainComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt new file mode 100644 index 0000000000..22592a4268 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onramp.main + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +internal interface OnrampMainComponent : ComposableContentComponent { + + class Params(val currency: String, val openSettings: () -> Unit) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt new file mode 100644 index 0000000000..99826bae5d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.main.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.main.model.OnrampMainComponentModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampMainComponentModelModule { + + @Binds + @IntoMap + @ClassKey(OnrampMainComponentModel::class) + fun bindOnrampSelectCountryModel(model: OnrampMainComponentModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt new file mode 100644 index 0000000000..78140491e5 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onramp.main.di + +import com.tangem.features.onramp.main.DefaultOnrampMainComponent +import com.tangem.features.onramp.main.OnrampMainComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampMainComponentModule { + + @Binds + @Singleton + fun bindOnrampMainComponentFactory(factory: DefaultOnrampMainComponent.Factory): OnrampMainComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt new file mode 100644 index 0000000000..03c2357074 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.main.entity + +import com.tangem.domain.onramp.model.OnrampCountry +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface OnrampMainBottomSheetConfig { + @Serializable + data class ConfirmResidency(val country: OnrampCountry) : OnrampMainBottomSheetConfig +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt new file mode 100644 index 0000000000..91f5ee356e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -0,0 +1,54 @@ +package com.tangem.features.onramp.main.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.onramp.impl.R + +@Immutable +internal sealed interface OnrampMainComponentUM { + + val topBarConfig: OnrampMainTopBarUM + val buyButtonConfig: BuyButtonConfig + + data class InitialLoading( + val currency: String, + val onClose: () -> Unit, + val openSettings: () -> Unit, + val onBuyClick: () -> Unit, + ) : OnrampMainComponentUM { + override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM( + title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), + startButtonUM = TopAppBarButtonUM( + iconRes = R.drawable.ic_close_24, + onIconClicked = onClose, + enabled = true, + ), + endButtonUM = TopAppBarButtonUM( + iconRes = R.drawable.ic_more_vertical_24, + onIconClicked = openSettings, + enabled = false, + ), + ) + + override val buyButtonConfig: BuyButtonConfig = BuyButtonConfig( + text = resourceReference(R.string.common_buy), + onClick = onBuyClick, + enabled = false, + ) + } + + data class Content( + override val topBarConfig: OnrampMainTopBarUM, + override val buyButtonConfig: BuyButtonConfig, + ) : OnrampMainComponentUM +} + +internal data class BuyButtonConfig( + val text: TextReference, + val onClick: () -> Unit, + val enabled: Boolean, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt new file mode 100644 index 0000000000..997292dfb8 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.main.entity + +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference + +data class OnrampMainTopBarUM( + val title: TextReference, + val startButtonUM: TopAppBarButtonUM, + val endButtonUM: TopAppBarButtonUM, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/transformer/OnrampAvailabilityTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/transformer/OnrampAvailabilityTransformer.kt new file mode 100644 index 0000000000..9da2af4521 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/transformer/OnrampAvailabilityTransformer.kt @@ -0,0 +1,53 @@ +package com.tangem.features.onramp.main.entity.transformer + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampAvailability +import com.tangem.domain.onramp.model.OnrampCountry +import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.utils.transformer.Transformer +import timber.log.Timber + +internal class OnrampAvailabilityTransformer( + private val maybeAvailabilityStatus: Either, + private val openBottomSheet: (OnrampMainBottomSheetConfig) -> Unit, +) : Transformer { + + override fun transform(prevState: OnrampMainComponentUM): OnrampMainComponentUM { + return maybeAvailabilityStatus.fold( + ifLeft = { + Timber.e(it) + prevState + }, + ifRight = { availability -> + when (availability) { + OnrampAvailability.Available -> handleAvailableStatus(prevState) + is OnrampAvailability.ConfirmResidency -> handleShowingConfirmResidency( + prevState = prevState, + country = availability.country, + ) + is OnrampAvailability.NotSupported -> handleShowingConfirmResidency( + prevState = prevState, + country = availability.country, + ) + } + }, + ) + } + + private fun handleAvailableStatus(prevState: OnrampMainComponentUM): OnrampMainComponentUM { + val endButton = prevState.topBarConfig.endButtonUM.copy(enabled = true) + return OnrampMainComponentUM.Content( + topBarConfig = prevState.topBarConfig.copy(endButtonUM = endButton), + buyButtonConfig = prevState.buyButtonConfig, + ) + } + + private fun handleShowingConfirmResidency( + prevState: OnrampMainComponentUM, + country: OnrampCountry, + ): OnrampMainComponentUM { + openBottomSheet.invoke(OnrampMainBottomSheetConfig.ConfirmResidency(country)) + return prevState + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt new file mode 100644 index 0000000000..97f7c33854 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -0,0 +1,65 @@ +package com.tangem.features.onramp.main.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.onramp.CheckOnrampAvailabilityUseCase +import com.tangem.features.onramp.main.OnrampMainComponent +import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.transformer.OnrampAvailabilityTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.transformer.Transformer +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal class OnrampMainComponentModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: OnrampMainComponent.Params = paramsContainer.require() + private val _state: MutableStateFlow = MutableStateFlow( + value = OnrampMainComponentUM.InitialLoading( + currency = params.currency, + onClose = router::pop, + openSettings = params.openSettings, + onBuyClick = ::onBuyClick, + ), + ) + val state: StateFlow get() = _state.asStateFlow() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + init { + checkResidenceCountry() + } + + private fun checkResidenceCountry() { + modelScope.launch { + OnrampAvailabilityTransformer( + maybeAvailabilityStatus = checkOnrampAvailabilityUseCase.invoke(), + openBottomSheet = ::openBottomSheet, + ).let(::updateState) + } + } + + @Suppress("EmptyFunctionBlock") + private fun onBuyClick() { + } + + private fun openBottomSheet(config: OnrampMainBottomSheetConfig) { + bottomSheetNavigation.activate(config) + } + + private fun updateState(transformer: Transformer) { + _state.update(transformer::transform) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt new file mode 100644 index 0000000000..bf30582c07 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt @@ -0,0 +1,100 @@ +package com.tangem.features.onramp.main.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM + +@Composable +internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier, + contentWindowInsets = WindowInsetsZero, + containerColor = TangemTheme.colors.background.secondary, + topBar = { + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + startButton = state.topBarConfig.startButtonUM, + endButton = state.topBarConfig.endButtonUM, + title = state.topBarConfig.title.resolveReference(), + ) + }, + content = { innerPadding -> + val contentModifier = Modifier + .padding(innerPadding) + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth() + .wrapContentHeight() + when (state) { + is OnrampMainComponentUM.InitialLoading -> InitialLoading(modifier = contentModifier) + is OnrampMainComponentUM.Content -> Content(modifier = contentModifier) + } + }, + floatingActionButton = { + BuyButton( + modifier = Modifier + .navigationBarsPadding() + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + onClick = state.buyButtonConfig.onClick, + enabled = state.buyButtonConfig.enabled, + ) + }, + floatingActionButtonPosition = FabPosition.Center, + ) +} + +@Composable +private fun InitialLoading(modifier: Modifier = Modifier) { + Column( + modifier = modifier + .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.background.action) + .padding(vertical = TangemTheme.dimens.spacing28), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size40)) + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16) + .size(width = TangemTheme.dimens.size96, height = TangemTheme.dimens.size24), + radius = TangemTheme.dimens.radius3, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16) + .size(width = TangemTheme.dimens.size72, height = TangemTheme.dimens.size12), + radius = TangemTheme.dimens.radius3, + ) + } +} + +@Composable +private fun Content(modifier: Modifier = Modifier) { + Box(modifier = modifier) +} + +@Composable +private fun BuyButton(onClick: () -> Unit, enabled: Boolean, modifier: Modifier = Modifier) { + PrimaryButton( + modifier = modifier, + text = stringResource(id = R.string.common_buy), + onClick = onClick, + enabled = enabled, + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/ConfirmResidencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/ConfirmResidencyModel.kt deleted file mode 100644 index 9457285bd3..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/ConfirmResidencyModel.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.features.onramp.model - -import com.tangem.core.decompose.di.ComponentScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.onramp.component.ConfirmResidencyComponent -import com.tangem.features.onramp.entity.ConfirmResidencyUM -import com.tangem.features.onramp.impl.R -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject - -@ComponentScoped -internal class ConfirmResidencyModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, - paramsContainer: ParamsContainer, -) : Model() { - - private val params: ConfirmResidencyComponent.Params = paramsContainer.require() - val state: MutableStateFlow = MutableStateFlow( - ConfirmResidencyUM( - country = params.countryName, - countryFlagUrl = params.countryFlagUrl, - isCountrySupported = params.isOnrampSupported, - primaryButtonConfig = getPrimaryButtonConfig(), - secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig( - onClick = ::onChangeClick, - text = resourceReference(R.string.common_change), - ), - ), - ) - - private fun getPrimaryButtonConfig() = if (params.isOnrampSupported) { - ConfirmResidencyUM.ActionButtonConfig( - onClick = params.onDismiss, - text = resourceReference(R.string.common_confirm), - ) - } else { - ConfirmResidencyUM.ActionButtonConfig( - onClick = ::onCloseClick, - text = resourceReference(R.string.common_close), - ) - } - - @Suppress("EmptyFunctionBlock") - private fun onCloseClick() { - } - - private fun onChangeClick() { - // TODO: [REDACTED_JIRA] - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/OnrampModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/OnrampModel.kt deleted file mode 100644 index d1f35e1fca..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/OnrampModel.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.features.onramp.model - -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.tangem.core.decompose.di.ComponentScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.decompose.navigation.Router -import com.tangem.features.onramp.component.OnrampComponent -import com.tangem.features.onramp.entity.OnrampBottomSheetConfig -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import javax.inject.Inject - -@Suppress("MagicNumber") -@ComponentScoped -internal class OnrampModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, - private val router: Router, - paramsContainer: ParamsContainer, -) : Model() { - - @Suppress("UnusedPrivateMember") - private val params: OnrampComponent.Params = paramsContainer.require() - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - - init { - modelScope.launch { - delay(1500) - bottomSheetNavigation.activate(OnrampBottomSheetConfig.ConfirmResidency) - } - } - - fun pop() { - router.pop() - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt new file mode 100644 index 0000000000..16d046651d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt @@ -0,0 +1,76 @@ +package com.tangem.features.onramp.root + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.onramp.component.OnrampComponent +import com.tangem.features.onramp.main.OnrampMainComponent +import com.tangem.features.onramp.root.entity.OnrampChild +import com.tangem.features.onramp.settings.OnrampSettingsComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultOnrampComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: OnrampComponent.Params, + private val settingsComponentFactory: OnrampSettingsComponent.Factory, + private val onrampMainComponentFactory: OnrampMainComponent.Factory, +) : OnrampComponent, AppComponentContext by context { + + private val navigation = StackNavigation() + private val contentStack = childStack( + key = "onramp_content_stack", + source = navigation, + serializer = OnrampChild.serializer(), + initialConfiguration = OnrampChild.Main, + handleBackButton = false, + childFactory = ::screenChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val childStack by contentStack.subscribeAsState() + + BackHandler(onBack = router::pop) + Children( + stack = childStack, + animation = stackAnimation(), + ) { child -> + child.instance.Content(modifier = modifier) + } + } + + private fun screenChild(config: OnrampChild, componentContext: ComponentContext): ComposableContentComponent = + when (config) { + OnrampChild.Settings -> settingsComponentFactory.create( + context = childByContext(componentContext), + params = OnrampSettingsComponent.Params(navigation::pop), + ) + OnrampChild.Main -> onrampMainComponentFactory.create( + context = childByContext(componentContext), + params = OnrampMainComponent.Params( + currency = params.currency, + openSettings = { navigation.push(OnrampChild.Settings) }, + ), + ) + } + + @AssistedFactory + interface Factory : OnrampComponent.Factory { + override fun create(context: AppComponentContext, params: OnrampComponent.Params): DefaultOnrampComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/di/OnrampComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/di/OnrampComponentModule.kt new file mode 100644 index 0000000000..9a1601386a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/di/OnrampComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onramp.root.di + +import com.tangem.features.onramp.component.OnrampComponent +import com.tangem.features.onramp.root.DefaultOnrampComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampComponentModule { + + @Binds + @Singleton + fun bindOnrampComponentFactory(factory: DefaultOnrampComponent.Factory): OnrampComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/entity/OnrampChild.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/entity/OnrampChild.kt new file mode 100644 index 0000000000..994a986713 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/entity/OnrampChild.kt @@ -0,0 +1,12 @@ +package com.tangem.features.onramp.root.entity + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class OnrampChild { + @Serializable + data object Main : OnrampChild() + + @Serializable + data object Settings : OnrampChild() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/DefaultSelectCountryComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/DefaultSelectCountryComponent.kt new file mode 100644 index 0000000000..35fc452dc8 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/DefaultSelectCountryComponent.kt @@ -0,0 +1,54 @@ +package com.tangem.features.onramp.selectcountry + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.onramp.selectcountry.model.OnrampSelectCountryModel +import com.tangem.features.onramp.selectcountry.ui.OnrampCountryList +import com.tangem.features.onramp.selectcountry.ui.SelectCountryBottomSheet +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultSelectCountryComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: SelectCountryComponent.Params, +) : SelectCountryComponent, AppComponentContext by appComponentContext { + + private val model: OnrampSelectCountryModel = getOrCreateModel(params) + + override fun dismiss() { + model.dismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.state.collectAsStateWithLifecycle() + val bottomSheetConfig = remember(key1 = this) { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + SelectCountryBottomSheet( + config = bottomSheetConfig, + content = { OnrampCountryList(state = state, modifier = Modifier.fillMaxSize()) }, + ) + } + + @AssistedFactory + interface Factory : SelectCountryComponent.Factory { + override fun create( + context: AppComponentContext, + params: SelectCountryComponent.Params, + ): DefaultSelectCountryComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/SelectCountryComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/SelectCountryComponent.kt new file mode 100644 index 0000000000..96a98be090 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/SelectCountryComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onramp.selectcountry + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent + +internal interface SelectCountryComponent : ComposableBottomSheetComponent { + + data class Params(val onDismiss: () -> Unit) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/di/OnrampSelectCountryComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/di/OnrampSelectCountryComponentModule.kt new file mode 100644 index 0000000000..86ac4bae7d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/di/OnrampSelectCountryComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.selectcountry.di + +import com.tangem.features.onramp.selectcountry.DefaultSelectCountryComponent +import com.tangem.features.onramp.selectcountry.SelectCountryComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampSelectCountryComponentModule { + + @Binds + @Singleton + fun bindOnrampSelectCountryComponentFactory( + factory: DefaultSelectCountryComponent.Factory, + ): SelectCountryComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/di/OnrampSelectCountryModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/di/OnrampSelectCountryModelModule.kt new file mode 100644 index 0000000000..1230e7174d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/di/OnrampSelectCountryModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.selectcountry.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.selectcountry.model.OnrampSelectCountryModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampSelectCountryModelModule { + + @Binds + @IntoMap + @ClassKey(OnrampSelectCountryModel::class) + fun bindOnrampSelectCountryModel(model: OnrampSelectCountryModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryItemState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryItemState.kt new file mode 100644 index 0000000000..033e746ae1 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryItemState.kt @@ -0,0 +1,27 @@ +package com.tangem.features.onramp.selectcountry.entity + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed interface CountryItemState { + /** Unique id */ + val id: String + + data class Loading(override val id: String) : CountryItemState + + sealed interface WithContent : CountryItemState { + data class Unavailable( + override val id: String, + val flagUrl: String, + val countryName: String, + ) : WithContent + + data class Content( + override val id: String, + val flagUrl: String, + val countryName: String, + val isSelected: Boolean, + val onClick: () -> Unit, + ) : WithContent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUM.kt new file mode 100644 index 0000000000..652407fced --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUM.kt @@ -0,0 +1,31 @@ +package com.tangem.features.onramp.selectcountry.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed interface CountryListUM { + + val searchBarUM: SearchBarUM + + data class Loading( + override val searchBarUM: SearchBarUM, + val items: ImmutableList, + ) : CountryListUM + + data class Error(override val searchBarUM: SearchBarUM, val onRetry: () -> Unit) : CountryListUM + + data class Content( + override val searchBarUM: SearchBarUM, + val items: ImmutableList, + ) : CountryListUM + + fun copySealed(searchBarUM: SearchBarUM = this.searchBarUM): CountryListUM { + return when (this) { + is Loading -> this.copy(searchBarUM = searchBarUM) + is Error -> this.copy(searchBarUM = searchBarUM) + is Content -> this.copy(searchBarUM = searchBarUM) + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUMController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUMController.kt new file mode 100644 index 0000000000..94114f2c2a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/CountryListUMController.kt @@ -0,0 +1,31 @@ +package com.tangem.features.onramp.selectcountry.entity + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.features.onramp.utils.SearchBarUMTransformer +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +internal class CountryListUMController( + searchBarUM: SearchBarUM, + loadingItems: ImmutableList, +) { + + val state: StateFlow get() = _state.asStateFlow() + private val _state: MutableStateFlow = + MutableStateFlow(value = CountryListUM.Loading(searchBarUM = searchBarUM, items = loadingItems)) + + fun update(transformer: Transformer) { + _state.update(transformer::transform) + } + + fun update(transformer: SearchBarUMTransformer) { + _state.update { prevState -> + val searchBarUM = transformer.transform(prevState.searchBarUM) + prevState.copySealed(searchBarUM) + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsLoadingTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsLoadingTransformer.kt new file mode 100644 index 0000000000..56f0b5f5cb --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsLoadingTransformer.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onramp.selectcountry.entity.transformer + +import com.tangem.features.onramp.selectcountry.entity.CountryItemState +import com.tangem.features.onramp.selectcountry.entity.CountryListUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList + +internal class UpdateCountryItemsLoadingTransformer( + private val loadingItems: ImmutableList, +) : Transformer { + override fun transform(prevState: CountryListUM): CountryListUM { + return CountryListUM.Loading(searchBarUM = prevState.searchBarUM, items = loadingItems) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsTransformer.kt new file mode 100644 index 0000000000..0005ddd810 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsTransformer.kt @@ -0,0 +1,57 @@ +package com.tangem.features.onramp.selectcountry.entity.transformer + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampCountry +import com.tangem.features.onramp.selectcountry.entity.CountryItemState +import com.tangem.features.onramp.selectcountry.entity.CountryListUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toImmutableList + +internal class UpdateCountryItemsTransformer( + private val maybeCountries: Either>, + private val defaultCountry: OnrampCountry?, + private val query: String, + private val onRetry: () -> Unit, + private val onCountryClick: (OnrampCountry) -> Unit, +) : Transformer { + + override fun transform(prevState: CountryListUM): CountryListUM { + return maybeCountries.fold( + ifLeft = { CountryListUM.Error(searchBarUM = prevState.searchBarUM, onRetry = onRetry) }, + ifRight = { countries -> + val countriesListItems = countries.filterByQuery().toUiModels() + CountryListUM.Content( + searchBarUM = prevState.searchBarUM, + items = countriesListItems.toImmutableList(), + ) + }, + ) + } + + private fun List.toUiModels(): List { + return map { country -> + if (country.onrampAvailable) { + CountryItemState.WithContent.Content( + id = country.id, + flagUrl = country.image, + countryName = country.name, + isSelected = defaultCountry?.id?.equals(country.id) == true, + onClick = { onCountryClick(country) }, + ) + } else { + CountryItemState.WithContent.Unavailable( + id = country.id, + flagUrl = country.image, + countryName = country.name, + ) + } + } + } + + private fun List.filterByQuery(): List { + return filter { country -> + country.alpha3.lowercase().contains(query.lowercase()) || + country.name.lowercase().contains(query.lowercase()) + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt new file mode 100644 index 0000000000..6dbb2cf7d5 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt @@ -0,0 +1,123 @@ +package com.tangem.features.onramp.selectcountry.model + +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.onramp.GetOnrampCountriesUseCase +import com.tangem.domain.onramp.GetOnrampCountryUseCase +import com.tangem.domain.onramp.OnrampSaveDefaultCountryUseCase +import com.tangem.domain.onramp.model.OnrampCountry +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.selectcountry.SelectCountryComponent +import com.tangem.features.onramp.selectcountry.entity.CountryItemState +import com.tangem.features.onramp.selectcountry.entity.CountryListUM +import com.tangem.features.onramp.selectcountry.entity.CountryListUMController +import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsLoadingTransformer +import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsTransformer +import com.tangem.features.onramp.utils.SearchManager +import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer +import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ComponentScoped +internal class OnrampSelectCountryModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val searchManager: SearchManager, + private val getOnrampCountriesUseCase: GetOnrampCountriesUseCase, + private val saveDefaultCountryUseCase: OnrampSaveDefaultCountryUseCase, + private val getOnrampCountryUseCase: GetOnrampCountryUseCase, + paramsContainer: ParamsContainer, +) : Model() { + + val state: StateFlow get() = countryListUMController.state + private val params: SelectCountryComponent.Params = paramsContainer.require() + private val countryListUMController = CountryListUMController( + searchBarUM = createSearchBarUM(), + loadingItems = loadingItems, + ) + private val refreshTrigger = MutableSharedFlow() + + init { + modelScope.launch { subscribeOnUpdateState() } + } + + fun dismiss() { + params.onDismiss() + } + + @OptIn(ExperimentalCoroutinesApi::class) + private suspend fun subscribeOnUpdateState() { + combine( + flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { flowOf(getOnrampCountriesUseCase.invoke()) }, + flow2 = getOnrampCountryUseCase.invoke(), + flow3 = searchManager.query, + ) { maybeCountries, maybeCountry, query -> + UpdateCountryItemsTransformer( + maybeCountries = maybeCountries, + defaultCountry = maybeCountry.getOrNull(), + query = query, + onRetry = ::onRetry, + onCountryClick = ::saveCountry, + ) + } + .onEach(countryListUMController::update) + .launchIn(modelScope) + } + + private fun saveCountry(country: OnrampCountry) { + modelScope.launch { + saveDefaultCountryUseCase.invoke(country) + dismiss() + } + } + + private fun onRetry() { + modelScope.launch { refreshTrigger.emit(Unit) } + countryListUMController.update(UpdateCountryItemsLoadingTransformer(loadingItems)) + } + + private fun onSearchQueryChange(newQuery: String) { + val searchBarUM = countryListUMController.state.value.searchBarUM + if (searchBarUM.query == newQuery) return + + modelScope.launch { + countryListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery)) + + searchManager.update(newQuery) + } + } + + private fun onSearchBarActiveChange(isActive: Boolean) { + countryListUMController.update( + transformer = UpdateSearchBarActiveStateTransformer( + isActive = isActive, + placeHolder = resourceReference(id = R.string.common_search), + ), + ) + } + + private fun createSearchBarUM(): SearchBarUM { + return SearchBarUM( + placeholderText = resourceReference(R.string.onramp_country_search), + query = "", + onQueryChange = ::onSearchQueryChange, + isActive = false, + onActiveChange = ::onSearchBarActiveChange, + ) + } + + private companion object { + private const val LOADING_ITEMS_COUNT = 5 + val loadingItems: ImmutableList = MutableList(LOADING_ITEMS_COUNT) { + CountryItemState.Loading("Loading #$it") + }.toImmutableList() + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt new file mode 100644 index 0000000000..6fbf4c8d16 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt @@ -0,0 +1,194 @@ +package com.tangem.features.onramp.selectcountry.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import coil.compose.AsyncImage +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.selectcountry.entity.CountryItemState +import com.tangem.features.onramp.selectcountry.entity.CountryListUM + +@Composable +internal fun SelectCountryBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { + TangemBottomSheet( + config = config, + content = { content() }, + ) +} + +@Composable +internal fun OnrampCountryList(state: CountryListUM, modifier: Modifier = Modifier) { + LazyColumn( + modifier = modifier, + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), + ) { + item(key = "search_bar") { + SearchBar( + state = state.searchBarUM, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing20), + ) + } + when (state) { + is CountryListUM.Content -> countryListWithContent(state = state) + is CountryListUM.Error -> countryListError(state = state) + is CountryListUM.Loading -> countryListLoading(state = state) + } + } +} + +private fun LazyListScope.countryListError(state: CountryListUM.Error) { + item(key = "error_content") { + Column( + modifier = Modifier + .fillParentMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(id = R.string.markets_loading_error_title), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + SecondarySmallButton( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + config = SmallButtonConfig( + text = resourceReference(id = R.string.try_to_load_data_again_button_title), + onClick = state.onRetry, + ), + ) + } + } +} + +private fun LazyListScope.countryListLoading(state: CountryListUM.Loading) { + items( + items = state.items, + key = { item -> item.id }, + contentType = { item -> item::class.java }, + itemContent = { _ -> LoadingCountryItem(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16)) }, + ) +} + +private fun LazyListScope.countryListWithContent(state: CountryListUM.Content) { + items( + items = state.items, + key = { item -> item.id }, + contentType = { item -> item::class.java }, + itemContent = { item -> + when (item) { + is CountryItemState.WithContent.Content -> ContentCountryItem( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = item.onClick) + .padding(vertical = TangemTheme.dimens.spacing16), + state = item, + ) + is CountryItemState.WithContent.Unavailable -> UnavailableCountryItem( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing16), + state = item, + ) + } + }, + ) +} + +@Composable +private fun LoadingCountryItem(modifier: Modifier = Modifier) { + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12)) { + CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size36)) + TextShimmer( + modifier = Modifier + .align(Alignment.CenterVertically) + .width(width = TangemTheme.dimens.size70), + style = TangemTheme.typography.subtitle2, + radius = TangemTheme.dimens.radius4, + ) + } +} + +@Composable +private fun ContentCountryItem(state: CountryItemState.WithContent.Content, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + AsyncImage( + modifier = Modifier.size(TangemTheme.dimens.size36), + model = state.flagUrl, + contentDescription = null, + ) + Text( + text = state.countryName, + modifier = Modifier.weight(1F), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + if (state.isSelected) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size24), + tint = TangemTheme.colors.icon.accent, + ) + } + } +} + +@Composable +@Suppress("MagicNumber") +private fun UnavailableCountryItem(state: CountryItemState.WithContent.Unavailable, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + AsyncImage( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .alpha(0.4F), + model = state.flagUrl, + contentDescription = null, + ) + Text( + modifier = Modifier.weight(1F), + text = state.countryName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.disabled, + ) + Text( + text = "Unavailable", + maxLines = 1, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/DefaultSelectCurrencyComponent.kt similarity index 52% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultConfirmResidencyComponent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/DefaultSelectCurrencyComponent.kt index 177e697eb8..687df5d015 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultConfirmResidencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/DefaultSelectCurrencyComponent.kt @@ -1,55 +1,56 @@ -package com.tangem.features.onramp.component.impl +package com.tangem.features.onramp.selectcurrency +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.features.onramp.component.ConfirmResidencyComponent -import com.tangem.features.onramp.model.ConfirmResidencyModel -import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheet -import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheetContent +import com.tangem.features.onramp.selectcurrency.model.OnrampSelectCurrencyModel +import com.tangem.features.onramp.selectcurrency.ui.OnrampCurrencyList +import com.tangem.features.onramp.selectcurrency.ui.SelectCurrencyBottomSheet import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -internal class DefaultConfirmResidencyComponent @AssistedInject constructor( +internal class DefaultSelectCurrencyComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted private val params: ConfirmResidencyComponent.Params, -) : ConfirmResidencyComponent, AppComponentContext by context { + @Assisted params: SelectCurrencyComponent.Params, +) : SelectCurrencyComponent, AppComponentContext by context { - private val model: ConfirmResidencyModel = getOrCreateModel(params) + private val model: OnrampSelectCurrencyModel = getOrCreateModel(params) override fun dismiss() { - params.onDismiss() + model.dismiss() } @Composable override fun BottomSheet() { val state by model.state.collectAsStateWithLifecycle() - val bottomSheetConfig = remember { + val bottomSheetConfig = remember(key1 = this) { TangemBottomSheetConfig( isShow = true, onDismissRequest = ::dismiss, content = TangemBottomSheetConfigContent.Empty, ) } - ConfirmResidencyBottomSheet( + SelectCurrencyBottomSheet( config = bottomSheetConfig, - content = { modifier -> - ConfirmResidencyBottomSheetContent(model = state, modifier = modifier) + content = { + OnrampCurrencyList(modifier = Modifier.fillMaxSize(), state = state) }, ) } @AssistedFactory - interface Factory : ConfirmResidencyComponent.Factory { + interface Factory : SelectCurrencyComponent.Factory { override fun create( context: AppComponentContext, - params: ConfirmResidencyComponent.Params, - ): DefaultConfirmResidencyComponent + params: SelectCurrencyComponent.Params, + ): DefaultSelectCurrencyComponent } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/SelectCurrencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/SelectCurrencyComponent.kt new file mode 100644 index 0000000000..a6c69ab8f3 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/SelectCurrencyComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onramp.selectcurrency + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent + +internal interface SelectCurrencyComponent : ComposableBottomSheetComponent { + + data class Params(val onDismiss: () -> Unit) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/di/OnrampSelectCurrencyComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/di/OnrampSelectCurrencyComponentModule.kt new file mode 100644 index 0000000000..f8ccf16d35 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/di/OnrampSelectCurrencyComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.selectcurrency.di + +import com.tangem.features.onramp.selectcurrency.DefaultSelectCurrencyComponent +import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampSelectCurrencyComponentModule { + + @Binds + @Singleton + fun bindSelectCurrencyComponentFactory( + factory: DefaultSelectCurrencyComponent.Factory, + ): SelectCurrencyComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/di/OnrampSelectCurrencyModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/di/OnrampSelectCurrencyModelModule.kt new file mode 100644 index 0000000000..3541b9cb45 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/di/OnrampSelectCurrencyModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.selectcurrency.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.selectcurrency.model.OnrampSelectCurrencyModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampSelectCurrencyModelModule { + + @Binds + @IntoMap + @ClassKey(OnrampSelectCurrencyModel::class) + fun bindOnrampSelectCurrencyModel(model: OnrampSelectCurrencyModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrenciesListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrenciesListUM.kt new file mode 100644 index 0000000000..91916eb2c4 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrenciesListUM.kt @@ -0,0 +1,31 @@ +package com.tangem.features.onramp.selectcurrency.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed interface CurrenciesListUM { + + val searchBarUM: SearchBarUM + + data class Loading( + override val searchBarUM: SearchBarUM, + val sections: ImmutableList>, + ) : CurrenciesListUM + + data class Error(override val searchBarUM: SearchBarUM, val onRetry: () -> Unit) : CurrenciesListUM + + data class Content( + override val searchBarUM: SearchBarUM, + val sections: ImmutableList>, + ) : CurrenciesListUM + + fun copySealed(searchBarUM: SearchBarUM = this.searchBarUM): CurrenciesListUM { + return when (this) { + is Loading -> this.copy(searchBarUM = searchBarUM) + is Error -> this.copy(searchBarUM = searchBarUM) + is Content -> this.copy(searchBarUM = searchBarUM) + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrenciesSection.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrenciesSection.kt new file mode 100644 index 0000000000..b15dffbba1 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrenciesSection.kt @@ -0,0 +1,6 @@ +package com.tangem.features.onramp.selectcurrency.entity + +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +internal data class CurrenciesSection(val title: TextReference, val items: ImmutableList) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrencyItemState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrencyItemState.kt new file mode 100644 index 0000000000..0e3c11fe59 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrencyItemState.kt @@ -0,0 +1,19 @@ +package com.tangem.features.onramp.selectcurrency.entity + +import androidx.compose.runtime.Immutable +import com.tangem.domain.onramp.model.OnrampCurrency + +@Immutable +internal sealed interface CurrencyItemState { + + /** Unique ID */ + val id: String + + data class Loading(override val id: String) : CurrencyItemState + + data class Content( + override val id: String, + val onrampCurrency: OnrampCurrency, + val onClick: () -> Unit, + ) : CurrencyItemState +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrencyListController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrencyListController.kt new file mode 100644 index 0000000000..eaad4b3365 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/CurrencyListController.kt @@ -0,0 +1,34 @@ +package com.tangem.features.onramp.selectcurrency.entity + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.features.onramp.utils.SearchBarUMTransformer +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +internal class CurrencyListController( + currencySearchBarUM: SearchBarUM, + loadingSections: ImmutableList>, +) { + val state: StateFlow get() = _state.asStateFlow() + private val _state: MutableStateFlow = MutableStateFlow( + value = CurrenciesListUM.Loading( + searchBarUM = currencySearchBarUM, + sections = loadingSections, + ), + ) + + fun update(transformer: Transformer) { + _state.update(transformer::transform) + } + + fun update(transformer: SearchBarUMTransformer) { + _state.update { prevState -> + val searchBarUM = transformer.transform(prevState.searchBarUM) + prevState.copySealed(searchBarUM) + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsLoadingTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsLoadingTransformer.kt new file mode 100644 index 0000000000..50eddcba44 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsLoadingTransformer.kt @@ -0,0 +1,16 @@ +package com.tangem.features.onramp.selectcurrency.entity.transformer + +import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM +import com.tangem.features.onramp.selectcurrency.entity.CurrenciesSection +import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList + +internal class UpdateCurrencyItemsLoadingTransformer( + private val loadingSections: ImmutableList>, +) : Transformer { + + override fun transform(prevState: CurrenciesListUM): CurrenciesListUM { + return CurrenciesListUM.Loading(searchBarUM = prevState.searchBarUM, sections = loadingSections) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsTransformer.kt new file mode 100644 index 0000000000..8e3782838b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsTransformer.kt @@ -0,0 +1,65 @@ +package com.tangem.features.onramp.selectcurrency.entity.transformer + +import arrow.core.Either +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.onramp.model.OnrampCurrencies +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM +import com.tangem.features.onramp.selectcurrency.entity.CurrenciesSection +import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toImmutableList + +internal class UpdateCurrencyItemsTransformer( + private val maybeCurrencies: Either, + private val query: String, + private val onRetry: () -> Unit, + private val onCurrencyClick: (OnrampCurrency) -> Unit, +) : Transformer { + + override fun transform(prevState: CurrenciesListUM): CurrenciesListUM { + return maybeCurrencies.fold( + ifLeft = { CurrenciesListUM.Error(searchBarUM = prevState.searchBarUM, onRetry = onRetry) }, + ifRight = { currencies -> + val populars = currencies.populars.filterByQuery() + val others = currencies.others.filterByQuery() + val sections = buildList { + if (populars.isNotEmpty()) { + add( + CurrenciesSection( + title = stringReference("Popular fiats"), + items = populars.map(::convertCurrencyToUiModel).toImmutableList(), + ), + ) + } + if (others.isNotEmpty()) { + add( + CurrenciesSection( + title = stringReference("Other currencies"), + items = others.map(::convertCurrencyToUiModel).toImmutableList(), + ), + ) + } + } + CurrenciesListUM.Content( + searchBarUM = prevState.searchBarUM, + sections = sections.toImmutableList(), + ) + }, + ) + } + + private fun List.filterByQuery(): List { + return filter { + it.code.lowercase().contains(query.lowercase()) || it.name.lowercase().contains(query.lowercase()) + } + } + + private fun convertCurrencyToUiModel(onrampCurrency: OnrampCurrency): CurrencyItemState.Content { + return CurrencyItemState.Content( + id = onrampCurrency.code, + onrampCurrency = onrampCurrency, + onClick = { onCurrencyClick(onrampCurrency) }, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt new file mode 100644 index 0000000000..35b364808d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt @@ -0,0 +1,125 @@ +package com.tangem.features.onramp.selectcurrency.model + +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.onramp.GetOnrampCurrenciesUseCase +import com.tangem.domain.onramp.OnrampSaveDefaultCurrencyUseCase +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent +import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM +import com.tangem.features.onramp.selectcurrency.entity.CurrenciesSection +import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState +import com.tangem.features.onramp.selectcurrency.entity.CurrencyListController +import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsLoadingTransformer +import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsTransformer +import com.tangem.features.onramp.utils.SearchManager +import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer +import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ComponentScoped +internal class OnrampSelectCurrencyModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val searchManager: SearchManager, + private val getOnrampCurrenciesUseCase: GetOnrampCurrenciesUseCase, + private val saveDefaultCurrencyUseCase: OnrampSaveDefaultCurrencyUseCase, + paramsContainer: ParamsContainer, +) : Model() { + + val state: StateFlow get() = controller.state + + private val params: SelectCurrencyComponent.Params = paramsContainer.require() + private val controller = CurrencyListController( + currencySearchBarUM = createSearchBarUM(), + loadingSections = loadingSections, + ) + private val refreshTrigger = MutableSharedFlow() + + init { + subscribeOnUpdateState() + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun subscribeOnUpdateState() { + combine( + flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { flowOf(getOnrampCurrenciesUseCase.invoke()) }, + flow2 = searchManager.query, + ) { maybeCurrencies, query -> + UpdateCurrencyItemsTransformer( + maybeCurrencies = maybeCurrencies, + query = query, + onRetry = ::onRetry, + onCurrencyClick = ::saveDefaultCurrency, + ) + } + .onEach(controller::update) + .launchIn(modelScope) + } + + fun dismiss() { + params.onDismiss() + } + + private fun saveDefaultCurrency(currency: OnrampCurrency) { + modelScope.launch { + saveDefaultCurrencyUseCase.invoke(currency) + dismiss() + } + } + + private fun onRetry() { + modelScope.launch { refreshTrigger.emit(Unit) } + controller.update(UpdateCurrencyItemsLoadingTransformer(loadingSections)) + } + + private fun onSearchQueryChange(newQuery: String) { + val searchBarUM = controller.state.value.searchBarUM + if (searchBarUM.query == newQuery) return + + modelScope.launch { + controller.update(transformer = UpdateSearchQueryTransformer(newQuery)) + + searchManager.update(newQuery) + } + } + + private fun onSearchBarActiveChange(isActive: Boolean) { + controller.update( + transformer = UpdateSearchBarActiveStateTransformer( + isActive = isActive, + placeHolder = resourceReference(id = R.string.common_search), + ), + ) + } + + private fun createSearchBarUM(): SearchBarUM { + return SearchBarUM( + placeholderText = resourceReference(R.string.onramp_currency_search), + query = "", + onQueryChange = ::onSearchQueryChange, + isActive = false, + onActiveChange = ::onSearchBarActiveChange, + ) + } + + private companion object { + val loadingSections = listOf( + CurrenciesSection(stringReference("Popular fiats"), items = createLoadingItems("popular")), + CurrenciesSection(stringReference("Other currencies"), items = createLoadingItems("other")), + ).toImmutableList() + + private fun createLoadingItems(prefix: String, size: Int = 5): ImmutableList = + MutableList(size) { index -> CurrencyItemState.Loading("$prefix#$index") }.toImmutableList() + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt new file mode 100644 index 0000000000..8177c32631 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt @@ -0,0 +1,175 @@ +package com.tangem.features.onramp.selectcurrency.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.util.fastForEach +import coil.compose.AsyncImage +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM +import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState + +@Composable +internal fun SelectCurrencyBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { + TangemBottomSheet( + config = config, + content = { content() }, + ) +} + +@Composable +internal fun OnrampCurrencyList(state: CurrenciesListUM, modifier: Modifier = Modifier) { + LazyColumn( + modifier = modifier, + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), + ) { + item(key = "search_bar") { + SearchBar( + state = state.searchBarUM, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing20), + ) + } + when (state) { + is CurrenciesListUM.Content -> currencyListContent(state = state) + is CurrenciesListUM.Error -> currencyListError(state = state) + is CurrenciesListUM.Loading -> currencyListLoading(state = state) + } + } +} + +private fun LazyListScope.currencyListError(state: CurrenciesListUM.Error) { + item(key = "error_content") { + Column( + modifier = Modifier + .fillParentMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(id = R.string.markets_loading_error_title), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + SecondarySmallButton( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + config = SmallButtonConfig( + text = resourceReference(id = R.string.try_to_load_data_again_button_title), + onClick = state.onRetry, + ), + ) + } + } +} + +private fun LazyListScope.currencyListLoading(state: CurrenciesListUM.Loading) { + state.sections.fastForEach { section -> + item(key = section.title.toString()) { GroupTitle(title = section.title.resolveReference()) } + items( + items = section.items, + key = { item -> item.id }, + contentType = { item -> item::class.java }, + itemContent = { CurrencyLoading(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16)) }, + ) + } +} + +private fun LazyListScope.currencyListContent(state: CurrenciesListUM.Content) { + state.sections.fastForEach { section -> + item(key = section.title.toString()) { GroupTitle(title = section.title.resolveReference()) } + items( + items = section.items, + key = { item -> item.id }, + contentType = { item -> item::class.java }, + itemContent = { item -> + CurrencyContent( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = item.onClick) + .padding(vertical = TangemTheme.dimens.spacing16), + currency = item, + ) + }, + ) + } +} + +@Composable +private fun GroupTitle(title: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = title, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun CurrencyContent(currency: CurrencyItemState.Content, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + AsyncImage( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(CircleShape), + model = currency.onrampCurrency.image, + contentDescription = null, + ) + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2)) { + Text( + text = currency.onrampCurrency.code, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = currency.onrampCurrency.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + +@Composable +private fun CurrencyLoading(modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size36)) + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6)) { + TextShimmer( + modifier = Modifier.width(width = TangemTheme.dimens.size70), + style = TangemTheme.typography.subtitle2, + ) + TextShimmer( + modifier = Modifier.width(width = TangemTheme.dimens.size52), + style = TangemTheme.typography.caption2, + ) + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt index 454ba6bea6..e144bfbffe 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt @@ -11,7 +11,7 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.onramp.entity.OnrampOperation +import com.tangem.features.onramp.tokenlist.entity.OnrampOperation import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selecttoken.ui.OnrampSelectToken import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent @@ -32,19 +32,22 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor( private val onrampTokenListComponent: OnrampTokenListComponent = onrampTokenListComponentFactory.create( context = child(key = "token_list"), params = OnrampTokenListComponent.Params( - filterOperation = params.operation, + filterOperation = when (params) { + is OnrampOperationComponent.Params.Buy -> OnrampOperation.BUY + is OnrampOperationComponent.Params.Sell -> OnrampOperation.SWAP + }, hasSearchBar = true, userWalletId = params.userWalletId, - onTokenClick = ::onTokenClick, + onTokenClick = { _, status -> onTokenClick(status) }, ), ) @Composable override fun Content(modifier: Modifier) { OnrampSelectToken( - titleResId = when (params.operation) { - OnrampOperation.BUY -> R.string.common_buy - OnrampOperation.SELL -> R.string.common_sell + titleResId = when (params) { + is OnrampOperationComponent.Params.Buy -> R.string.common_buy + is OnrampOperationComponent.Params.Sell -> R.string.common_sell }, onBackClick = router::pop, onrampTokenListComponent = onrampTokenListComponent, @@ -57,9 +60,9 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor( val appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }.code reduxStateHolder.dispatch( - when (params.operation) { - OnrampOperation.BUY -> getBuyAction(status, appCurrencyCode) - OnrampOperation.SELL -> TradeCryptoAction.Sell(status, appCurrencyCode) + action = when (params) { + is OnrampOperationComponent.Params.Buy -> getBuyAction(status, appCurrencyCode) + is OnrampOperationComponent.Params.Sell -> TradeCryptoAction.Sell(status, appCurrencyCode) }, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/OnrampOperationComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/OnrampOperationComponent.kt index c35a452d58..74878fed83 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/OnrampOperationComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/OnrampOperationComponent.kt @@ -3,7 +3,6 @@ package com.tangem.features.onramp.selecttoken import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.onramp.entity.OnrampOperation /** * Base operation component @@ -17,8 +16,14 @@ internal interface OnrampOperationComponent : ComposableContentComponent { /** * Params * - * @property operation operation * @property userWalletId id of multi-currency wallet */ - data class Params(val operation: OnrampOperation, val userWalletId: UserWalletId) + sealed interface Params { + + val userWalletId: UserWalletId + + data class Buy(override val userWalletId: UserWalletId) : Params + + data class Sell(override val userWalletId: UserWalletId) : Params + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt index 653883fb90..8d66dfa574 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt @@ -2,11 +2,12 @@ package com.tangem.features.onramp.selecttoken.ui import androidx.activity.compose.BackHandler import androidx.annotation.StringRes +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -16,6 +17,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun OnrampSelectToken( @StringRes titleResId: Int, @@ -25,21 +27,25 @@ internal fun OnrampSelectToken( ) { BackHandler(onBack = onBackClick) - Column( + LazyColumn( modifier = modifier .background(TangemTheme.colors.background.secondary) .imePadding() .systemBarsPadding(), ) { - AppBarWithBackButton( - onBackClick = onBackClick, - text = stringResource(id = titleResId), - iconRes = R.drawable.ic_close_24, - ) + stickyHeader(key = "header") { + AppBarWithBackButton( + onBackClick = onBackClick, + text = stringResource(id = titleResId), + iconRes = R.drawable.ic_close_24, + containerColor = TangemTheme.colors.background.secondary, + ) + } - onrampTokenListComponent.Content( - contentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp), - modifier = Modifier, - ) + item(key = "token_list", contentType = "token_list") { + onrampTokenListComponent.Content( + modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp), + ) + } } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/sell/DefaultSellCryptoComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/sell/DefaultSellCryptoComponent.kt index f7d63936d1..db922732cf 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/sell/DefaultSellCryptoComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/sell/DefaultSellCryptoComponent.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext import com.tangem.features.onramp.component.SellCryptoComponent -import com.tangem.features.onramp.entity.OnrampOperation import com.tangem.features.onramp.selecttoken.OnrampOperationComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -20,7 +19,7 @@ internal class DefaultSellCryptoComponent @AssistedInject constructor( private val selectTokenComponent: OnrampOperationComponent = onrampOperationComponentFactory.create( context = appComponentContext, - params = OnrampOperationComponent.Params(operation = OnrampOperation.SELL, userWalletId = params.userWalletId), + params = OnrampOperationComponent.Params.Sell(userWalletId = params.userWalletId), ) @Composable diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/DefaultOnrampSettingsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/DefaultOnrampSettingsComponent.kt new file mode 100644 index 0000000000..efec6676c4 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/DefaultOnrampSettingsComponent.kt @@ -0,0 +1,65 @@ +package com.tangem.features.onramp.settings + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.onramp.selectcountry.SelectCountryComponent +import com.tangem.features.onramp.settings.entity.OnrampSettingsConfig +import com.tangem.features.onramp.settings.model.OnrampSettingsModel +import com.tangem.features.onramp.settings.ui.OnrampSettingsContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnrampSettingsComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: OnrampSettingsComponent.Params, + private val selectCountryComponentFactory: SelectCountryComponent.Factory, +) : OnrampSettingsComponent, AppComponentContext by appComponentContext { + + private val model: OnrampSettingsModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + BackHandler(onBack = params.onBack) + OnrampSettingsContent(modifier = modifier, state = state) + + val bottomSheet by bottomSheetSlot.subscribeAsState() + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: OnrampSettingsConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + OnrampSettingsConfig.SelectCountry -> selectCountryComponentFactory.create( + context = childByContext(componentContext), + params = SelectCountryComponent.Params(model.bottomSheetNavigation::dismiss), + ) + } + + @AssistedFactory + interface Factory : OnrampSettingsComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnrampSettingsComponent.Params, + ): DefaultOnrampSettingsComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/OnrampSettingsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/OnrampSettingsComponent.kt new file mode 100644 index 0000000000..2514bbb00b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/OnrampSettingsComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onramp.settings + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +internal interface OnrampSettingsComponent : ComposableContentComponent { + + data class Params(val onBack: () -> Unit) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/di/OnrampSettingsComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/di/OnrampSettingsComponentModule.kt new file mode 100644 index 0000000000..648bf03301 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/di/OnrampSettingsComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.settings.di + +import com.tangem.features.onramp.settings.DefaultOnrampSettingsComponent +import com.tangem.features.onramp.settings.OnrampSettingsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampSettingsComponentModule { + + @Binds + @Singleton + fun bindOnrampSettingsComponentFactory( + factory: DefaultOnrampSettingsComponent.Factory, + ): OnrampSettingsComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/di/OnrampSettingsModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/di/OnrampSettingsModelModule.kt new file mode 100644 index 0000000000..f62678e7ee --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/di/OnrampSettingsModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.settings.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.settings.model.OnrampSettingsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampSettingsModelModule { + + @Binds + @IntoMap + @ClassKey(OnrampSettingsModel::class) + fun bindOnrampSettingsModelModel(model: OnrampSettingsModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/entity/OnrampSettingsConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/entity/OnrampSettingsConfig.kt new file mode 100644 index 0000000000..11ba9a2f0b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/entity/OnrampSettingsConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.features.onramp.settings.entity + +import kotlinx.serialization.Serializable + +@Serializable +sealed class OnrampSettingsConfig { + + data object SelectCountry : OnrampSettingsConfig() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/entity/OnrampSettingsItemUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/entity/OnrampSettingsItemUM.kt new file mode 100644 index 0000000000..9bd7ad60a1 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/entity/OnrampSettingsItemUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.onramp.settings.entity + +sealed class OnrampSettingsItemUM { + data class Residence( + val countryName: String = "", + val flagUrl: String = "", + val onClick: () -> Unit, + ) : OnrampSettingsItemUM() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/entity/OnrampSettingsUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/entity/OnrampSettingsUM.kt new file mode 100644 index 0000000000..0997eb67d1 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/entity/OnrampSettingsUM.kt @@ -0,0 +1,8 @@ +package com.tangem.features.onramp.settings.entity + +import kotlinx.collections.immutable.ImmutableList + +internal data class OnrampSettingsUM( + val onBack: () -> Unit, + val items: ImmutableList, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/model/OnrampSettingsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/model/OnrampSettingsModel.kt new file mode 100644 index 0000000000..0f41e6f6ca --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/model/OnrampSettingsModel.kt @@ -0,0 +1,69 @@ +package com.tangem.features.onramp.settings.model + +import arrow.core.Either +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.onramp.GetOnrampCountryUseCase +import com.tangem.domain.onramp.model.OnrampCountry +import com.tangem.features.onramp.settings.OnrampSettingsComponent +import com.tangem.features.onramp.settings.entity.OnrampSettingsConfig +import com.tangem.features.onramp.settings.entity.OnrampSettingsItemUM +import com.tangem.features.onramp.settings.entity.OnrampSettingsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@ComponentScoped +internal class OnrampSettingsModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val getOnrampCountryUseCase: GetOnrampCountryUseCase, + paramsContainer: ParamsContainer, +) : Model() { + + val state: StateFlow get() = _state + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + private val params: OnrampSettingsComponent.Params = paramsContainer.require() + private val _state: MutableStateFlow = MutableStateFlow(getInitialState()) + + init { + subscribeOnUpdateState() + } + + private fun subscribeOnUpdateState() { + getOnrampCountryUseCase.invoke() + .onEach(::updateResidenceState) + .launchIn(modelScope) + } + + private fun updateResidenceState(maybeCountry: Either) { + maybeCountry.onRight { country -> + _state.update { state -> + state.copy( + items = listOf( + OnrampSettingsItemUM.Residence( + countryName = country?.name.orEmpty(), + flagUrl = country?.image.orEmpty(), + onClick = ::openSelectCountryBottomSheet, + ), + ).toImmutableList(), + ) + } + } + } + + private fun openSelectCountryBottomSheet() { + bottomSheetNavigation.activate(OnrampSettingsConfig.SelectCountry) + } + + private fun getInitialState(): OnrampSettingsUM { + return OnrampSettingsUM( + onBack = params.onBack, + items = listOf(OnrampSettingsItemUM.Residence(onClick = ::openSelectCountryBottomSheet)).toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt new file mode 100644 index 0000000000..949a3af30e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt @@ -0,0 +1,111 @@ +package com.tangem.features.onramp.settings.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.util.fastForEach +import coil.compose.AsyncImage +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.settings.entity.OnrampSettingsItemUM +import com.tangem.features.onramp.settings.entity.OnrampSettingsUM +import kotlinx.collections.immutable.ImmutableList + +@Composable +internal fun OnrampSettingsContent(state: OnrampSettingsUM, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier, + contentWindowInsets = WindowInsetsZero, + containerColor = TangemTheme.colors.background.secondary, + topBar = { + AppBarWithBackButton( + modifier = Modifier.statusBarsPadding(), + onBackClick = state.onBack, + text = stringResource(id = R.string.onramp_settings_title), + iconRes = R.drawable.ic_close_24, + ) + }, + content = { innerPadding -> + Content( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .padding(horizontal = TangemTheme.dimens.spacing16), + items = state.items, + ) + }, + ) +} + +@Composable +private fun Content(items: ImmutableList, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + items.fastForEach { model -> + key(model) { + when (model) { + is OnrampSettingsItemUM.Residence -> ResidenceSection(model) + } + } + } + } +} + +@Composable +private fun ResidenceSection(state: OnrampSettingsItemUM.Residence, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.background.primary) + .clickable(onClick = state.onClick) + .padding(TangemTheme.dimens.spacing12) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.onramp_settings_residence), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) + SpacerWMax() + AsyncImage( + modifier = Modifier.size(TangemTheme.dimens.size20), + model = state.flagUrl, + contentDescription = null, + ) + Text( + text = state.countryName, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2, + ) + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + Text( + text = stringResource(id = R.string.onramp_settings_residence_description), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt new file mode 100644 index 0000000000..457590ea6b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt @@ -0,0 +1,70 @@ +package com.tangem.features.onramp.swap + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.onramp.component.SwapSelectTokensComponent +import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent +import com.tangem.features.onramp.swap.model.SwapSelectTokensModel +import com.tangem.features.onramp.swap.ui.SwapSelectTokens +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +import com.tangem.features.onramp.tokenlist.entity.OnrampOperation +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( + tokenListComponentFactory: OnrampTokenListComponent.Factory, + availableSwapPairsComponentFactory: AvailableSwapPairsComponent.Factory, + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: SwapSelectTokensComponent.Params, +) : AppComponentContext by appComponentContext, SwapSelectTokensComponent { + + private val model: SwapSelectTokensModel = getOrCreateModel(params) + + private val selectFromTokenListComponent: OnrampTokenListComponent = tokenListComponentFactory.create( + context = child(key = "select_from_token_list"), + params = OnrampTokenListComponent.Params( + filterOperation = OnrampOperation.SWAP, + hasSearchBar = true, + userWalletId = params.userWalletId, + onTokenClick = model::selectFromToken, + ), + ) + + private val selectToTokenListComponent: AvailableSwapPairsComponent = + availableSwapPairsComponentFactory.create( + context = child(key = "select_to_token_list"), + params = AvailableSwapPairsComponent.Params( + userWalletId = params.userWalletId, + selectedStatus = model.fromCurrencyStatus, + onTokenClick = model::selectToToken, + ), + ) + + @Composable + override fun Content(modifier: Modifier) { + val state = model.state.collectAsStateWithLifecycle() + + SwapSelectTokens( + state = state.value, + selectFromTokenListComponent = selectFromTokenListComponent, + selectToTokenListComponent = selectToTokenListComponent, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : SwapSelectTokensComponent.Factory { + + override fun create( + context: AppComponentContext, + params: SwapSelectTokensComponent.Params, + ): DefaultSwapSelectTokensComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt new file mode 100644 index 0000000000..93b1be67aa --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt @@ -0,0 +1,30 @@ +package com.tangem.features.onramp.swap.availablepairs + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.StateFlow + +/** Token list component that present list of available tokens for swap */ +@Stable +internal interface AvailableSwapPairsComponent : ComposableContentComponent { + + /** Component factory */ + interface Factory : ComponentFactory + + /** + * Params + * + * @property userWalletId id of multi-currency wallet + * @property selectedStatus flow of selected status + * @property onTokenClick callback for token click + */ + data class Params( + val userWalletId: UserWalletId, + val selectedStatus: StateFlow, + val onTokenClick: (TokenItemState, CryptoCurrencyStatus) -> Unit, + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt new file mode 100644 index 0000000000..fd77ee062f --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt @@ -0,0 +1,38 @@ +package com.tangem.features.onramp.swap.availablepairs + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel +import com.tangem.features.onramp.tokenlist.ui.TokenList +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: AvailableSwapPairsComponent.Params, +) : AvailableSwapPairsComponent, AppComponentContext by context { + + private val model: AvailableSwapPairsModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + TokenList(state = state, modifier = modifier) + } + + @AssistedFactory + interface Factory : AvailableSwapPairsComponent.Factory { + override fun create( + context: AppComponentContext, + params: AvailableSwapPairsComponent.Params, + ): DefaultAvailableSwapPairsComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/di/AvailableSwapPairsComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/di/AvailableSwapPairsComponentModule.kt new file mode 100644 index 0000000000..0f451e0008 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/di/AvailableSwapPairsComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.swap.availablepairs.di + +import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent +import com.tangem.features.onramp.swap.availablepairs.DefaultAvailableSwapPairsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface AvailableSwapPairsComponentModule { + + @Binds + @Singleton + fun bindAvailableSwapPairsComponentFactory( + factory: DefaultAvailableSwapPairsComponent.Factory, + ): AvailableSwapPairsComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/di/AvailableSwapPairsModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/di/AvailableSwapPairsModelModule.kt new file mode 100644 index 0000000000..6bf7c67133 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/di/AvailableSwapPairsModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.swap.availablepairs.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface AvailableSwapPairsModelModule { + + @Binds + @IntoMap + @ClassKey(AvailableSwapPairsModel::class) + fun bindAvailableSwapPairsModel(model: AvailableSwapPairsModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt new file mode 100644 index 0000000000..056bbaf84f --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt @@ -0,0 +1,26 @@ +package com.tangem.features.onramp.swap.availablepairs.entity.converters + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.converter.Converter + +/** + * Convert from [CryptoCurrencyStatus] to loading [TokensListItemUM.Token] state + * +[REDACTED_AUTHOR] + */ +internal object LoadingTokenListItemConverter : Converter { + + override fun convert(value: CryptoCurrencyStatus): TokensListItemUM.Token { + return TokensListItemUM.Token( + state = TokenItemState.Loading( + id = value.currency.id.value, + iconState = CurrencyIconState.Loading, + titleState = TokenItemState.TitleState.Loading, + subtitleState = TokenItemState.SubtitleState.Loading, + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt new file mode 100644 index 0000000000..b14b3a023e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.features.onramp.swap.availablepairs.entity.transformers + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer +import kotlinx.collections.immutable.toImmutableList + +/** + * Set [statuses] as loading items + * +[REDACTED_AUTHOR] + */ +internal class SetLoadingTokenItemsTransformer( + private val statuses: List, +) : TokenListUMTransformer { + + override fun transform(prevState: TokenListUM): TokenListUM { + return prevState.copy( + availableItems = LoadingTokenListItemConverter.convertList(input = statuses).toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt new file mode 100644 index 0000000000..03a36792af --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -0,0 +1,185 @@ +package com.tangem.features.onramp.swap.availablepairs.model + +import arrow.core.getOrElse +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.capitalize +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.core.utils.getOrElse +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.swap.domain.GetAvailablePairsUseCase +import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo +import com.tangem.feature.swap.domain.models.domain.SwapPairLeast +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent +import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMController +import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer +import com.tangem.features.onramp.utils.SearchManager +import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer +import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class AvailableSwapPairsModel @Inject constructor( + paramsContainer: ParamsContainer, + getTokenListUseCase: GetTokenListUseCase, + override val dispatchers: CoroutineDispatcherProvider, + private val tokenListUMController: TokenListUMController, + private val searchManager: SearchManager, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getAvailablePairsUseCase: GetAvailablePairsUseCase, +) : Model() { + + val state: StateFlow = tokenListUMController.state + + private var params: AvailableSwapPairsComponent.Params = paramsContainer.require() + + private val tokenListFlow = getTokenListUseCase.launch(userWalletId = params.userWalletId) + .distinctUntilChanged() + .map { maybeTokenList -> + maybeTokenList.getOrElse( + ifLoading = { it ?: TokenList.Empty }, + ifError = { TokenList.Empty }, + ) + .flattenCurrencies() + } + .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) + + private val availablePairsByNetworkFlow = MutableStateFlow>>(emptyMap()) + + init { + subscribeOnUpdateState() + subscribeOnAvailablePairsUpdates() + } + + private fun subscribeOnUpdateState() { + combine( + flow = tokenListFlow, + flow2 = getAppCurrencyAndBalanceHidingFlow(), + flow3 = params.selectedStatus, + flow4 = searchManager.query, + flow5 = availablePairsByNetworkFlow + .map { it[params.selectedStatus.value?.toLeastTokenInfo()].orEmpty() } + .distinctUntilChanged(), + ) { currencies, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairs -> + if (availablePairs.isEmpty()) { + SetLoadingTokenItemsTransformer(currencies) + } else { + val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding + + val filterTokenList = currencies + .filterByQuery(query = query) + .filterByAvailability(availablePairs = availablePairs) + + UpdateTokenItemsTransformer( + appCurrency = appCurrency, + onItemClick = params.onTokenClick, + statuses = filterTokenList, + isBalanceHidden = isBalanceHidden, + hasSearchBar = currencies.isNotEmpty(), + unavailableTokensHeaderReference = resourceReference( + id = R.string.tokens_list_unavailable_to_swap_header, + wrappedList(selectedStatus?.currency?.name?.capitalize() ?: ""), + ), + onQueryChange = ::onSearchQueryChange, + onActiveChange = ::onSearchBarActiveChange, + ) + } + } + .onEach(tokenListUMController::update) + .flowOn(dispatchers.main) + .launchIn(modelScope) + } + + private fun subscribeOnAvailablePairsUpdates() { + modelScope.launch { + params.selectedStatus + .filterNotNull() + .collectLatest { selectedStatus -> + val initialCurrency = selectedStatus.toLeastTokenInfo() + val tokenList = tokenListFlow.firstOrNull() ?: return@collectLatest + + val availablePairs = availablePairsByNetworkFlow.value[initialCurrency] + if (!availablePairs.isNullOrEmpty()) return@collectLatest + + val pairs = getAvailablePairsUseCase( + initialCurrency = initialCurrency, + currencies = tokenList.map(CryptoCurrencyStatus::currency), + ) + + availablePairsByNetworkFlow.update { + it.toMutableMap().apply { + put(initialCurrency, pairs) + } + } + } + } + } + + private fun getAppCurrencyAndBalanceHidingFlow(): Flow> { + return combine( + flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(), + flow2 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(), + transform = ::Pair, + ) + } + + private fun onSearchQueryChange(newQuery: String) { + val searchBar = tokenListUMController.getSearchBar() + if (searchBar?.searchBarUM?.query == newQuery) return + + modelScope.launch { + tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery)) + + searchManager.update(newQuery) + } + } + + private fun onSearchBarActiveChange(isActive: Boolean) { + tokenListUMController.update( + transformer = UpdateSearchBarActiveStateTransformer( + isActive = isActive, + placeHolder = resourceReference(id = R.string.common_search), + ), + ) + } + + private fun List.filterByQuery(query: String): List { + return filter { + it.currency.name.contains(other = query, ignoreCase = true) || + it.currency.symbol.contains(other = query, ignoreCase = true) + } + } + + private fun List.filterByAvailability( + availablePairs: List, + ): Map> { + return groupBy { status -> + val isAvailable = availablePairs.map(SwapPairLeast::to).contains(status.toLeastTokenInfo()) + + isAvailable && + status.value !is CryptoCurrencyStatus.MissedDerivation && + status.value !is CryptoCurrencyStatus.Unreachable + } + } + + private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo { + return LeastTokenInfo( + contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", + network = currency.network.backendId, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensComponentModule.kt new file mode 100644 index 0000000000..059c28a82a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.swap.di + +import com.tangem.features.onramp.component.SwapSelectTokensComponent +import com.tangem.features.onramp.swap.DefaultSwapSelectTokensComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface SwapSelectTokensComponentModule { + + @Binds + @Singleton + fun bindSwapSelectTokensComponentFactory( + factory: DefaultSwapSelectTokensComponent.Factory, + ): SwapSelectTokensComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensModelModule.kt new file mode 100644 index 0000000000..f7e0741156 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/di/SwapSelectTokensModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.swap.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.swap.model.SwapSelectTokensModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface SwapSelectTokensModelModule { + + @Binds + @IntoMap + @ClassKey(SwapSelectTokensModel::class) + fun bindOnrampTokenListModel(model: SwapSelectTokensModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt index 07c60222b4..11486011da 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.swap.entity import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.onramp.impl.R /** @@ -15,8 +16,8 @@ internal sealed interface ExchangeCardUM { /** Title reference */ val titleReference: TextReference - /** Flag that indicates if remove button should be shown */ - val hasRemoveButton: Boolean + /** Remove button UI model */ + val removeButtonUM: RemoveButtonUM? /** Token item state */ val tokenItemState: TokenItemState @@ -24,25 +25,26 @@ internal sealed interface ExchangeCardUM { /** * Empty state * - * @property titleReference title reference - * @property onItemClick callback which will be called when an item is clicked + * @property titleReference title reference + * @property subtitleReference empty token subtitle reference */ data class Empty( override val titleReference: TextReference, - val onItemClick: () -> Unit, + val subtitleReference: TextReference, ) : ExchangeCardUM { - override val hasRemoveButton: Boolean = false + override val removeButtonUM: RemoveButtonUM? = null - // TODO: [REDACTED_JIRA] override val tokenItemState: TokenItemState = TokenItemState.Content( id = "empty", iconState = CurrencyIconState.Empty(R.drawable.ic_empty_64), - titleState = TokenItemState.TitleState.Content(text = "Choose the Token"), - subtitleState = TokenItemState.SubtitleState.TextContent(value = "You want to Swap"), + titleState = TokenItemState.TitleState.Content( + text = resourceReference(id = R.string.action_buttons_swap_choose_token), + ), + subtitleState = TokenItemState.SubtitleState.TextContent(value = subtitleReference), fiatAmountState = TokenItemState.FiatAmountState.Content(text = ""), subtitle2State = TokenItemState.Subtitle2State.TextContent(text = ""), - onItemClick = onItemClick, + onItemClick = null, onItemLongClick = null, ) } @@ -51,13 +53,14 @@ internal sealed interface ExchangeCardUM { * Filled * * @property titleReference title reference + * @property removeButtonUM remove button UI model * @property tokenItemState token item state */ data class Filled( override val titleReference: TextReference, + override val removeButtonUM: RemoveButtonUM?, override val tokenItemState: TokenItemState, - ) : ExchangeCardUM { + ) : ExchangeCardUM - override val hasRemoveButton: Boolean = true - } + data class RemoveButtonUM(val onClick: () -> Unit) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt new file mode 100644 index 0000000000..f6cd655930 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt @@ -0,0 +1,42 @@ +package com.tangem.features.onramp.swap.entity + +import com.tangem.core.decompose.navigation.Router +import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeFrom +import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber +import javax.inject.Inject + +/** + * [SwapSelectTokensUM] controller + * + * @property router router + * +[REDACTED_AUTHOR] + */ +internal class SwapSelectTokensController @Inject constructor( + private val router: Router, +) { + + val state: StateFlow get() = _state + + private val _state: MutableStateFlow = MutableStateFlow( + value = SwapSelectTokensUM( + onBackClick = router::pop, + exchangeFrom = createEmptyExchangeFrom(), + exchangeTo = createEmptyExchangeTo(), + ), + ) + + fun update(transform: (SwapSelectTokensUM) -> SwapSelectTokensUM) { + Timber.d("Applying non-name transformation") + _state.update(transform) + } + + fun update(transformer: SwapSelectTokensUMTransformer) { + Timber.d("Applying ${transformer::class.simpleName}") + _state.update(transformer::transform) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUM.kt new file mode 100644 index 0000000000..7defa5ca8f --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUM.kt @@ -0,0 +1,16 @@ +package com.tangem.features.onramp.swap.entity + +/** + * Swap select tokens UI model + * + * @property onBackClick callback is called when back button is clicked + * @property exchangeFrom exchange "from" card UI model + * @property exchangeTo exchange "to" card UI model + * +[REDACTED_AUTHOR] + */ +internal data class SwapSelectTokensUM( + val onBackClick: () -> Unit, + val exchangeFrom: ExchangeCardUM, + val exchangeTo: ExchangeCardUM, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUMTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUMTransformer.kt new file mode 100644 index 0000000000..fb2f9ca537 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensUMTransformer.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.swap.entity + +import com.tangem.utils.transformer.Transformer + +/** + * Base [SwapSelectTokensUM] transformer + * +[REDACTED_AUTHOR] + */ +internal interface SwapSelectTokensUMTransformer : Transformer \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/RemoveSelectedFromTokenTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/RemoveSelectedFromTokenTransformer.kt new file mode 100644 index 0000000000..828f9d6ad0 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/RemoveSelectedFromTokenTransformer.kt @@ -0,0 +1,21 @@ +package com.tangem.features.onramp.swap.entity.transformer + +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer +import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeFrom +import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo + +/** + * Transformer for removing selected "from" token + * +[REDACTED_AUTHOR] + */ +internal object RemoveSelectedFromTokenTransformer : SwapSelectTokensUMTransformer { + + override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM { + return prevState.copy( + exchangeFrom = createEmptyExchangeFrom(), + exchangeTo = createEmptyExchangeTo(), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt new file mode 100644 index 0000000000..08c088f59e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.features.onramp.swap.entity.transformer + +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.features.onramp.swap.entity.ExchangeCardUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer +import com.tangem.features.onramp.swap.entity.utils.toFilled + +/** + * Transformer for selecting "from" token + * + * @property selectedTokenItemState token item state + * @property onRemoveClick callback is called when remove button is clicked + * +[REDACTED_AUTHOR] + */ +internal class SelectFromTokenTransformer( + private val selectedTokenItemState: TokenItemState, + private val onRemoveClick: () -> Unit, +) : SwapSelectTokensUMTransformer { + + override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM { + return prevState.copy( + exchangeFrom = prevState.exchangeFrom.toFilled( + selectedTokenItemState = selectedTokenItemState, + removeButtonUM = ExchangeCardUM.RemoveButtonUM(onClick = onRemoveClick), + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt new file mode 100644 index 0000000000..433b28f5ff --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.features.onramp.swap.entity.transformer + +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.features.onramp.swap.entity.ExchangeCardUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer +import com.tangem.features.onramp.swap.entity.utils.toFilled + +/** + * Transformer for selecting "to" token + * + * @property selectedTokenItemState token item state + * +[REDACTED_AUTHOR] + */ +internal class SelectToTokenTransformer( + private val selectedTokenItemState: TokenItemState, +) : SwapSelectTokensUMTransformer { + + override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM { + return prevState.copy( + exchangeFrom = prevState.exchangeFrom.hideRemoveButton(), + exchangeTo = prevState.exchangeTo.toFilled(selectedTokenItemState = selectedTokenItemState), + ) + } + + private fun ExchangeCardUM.hideRemoveButton(): ExchangeCardUM { + return (this as? ExchangeCardUM.Filled)?.copy(removeButtonUM = null) ?: this + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt new file mode 100644 index 0000000000..6daf403437 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt @@ -0,0 +1,39 @@ +package com.tangem.features.onramp.swap.entity.utils + +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.swap.entity.ExchangeCardUM + +/** Create empty exchange "from" card */ +internal fun createEmptyExchangeFrom(): ExchangeCardUM.Empty { + return ExchangeCardUM.Empty( + titleReference = resourceReference(id = R.string.swapping_from_title), + subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap), + ) +} + +/** Create empty exchange "to" card */ +internal fun createEmptyExchangeTo(): ExchangeCardUM.Empty { + return ExchangeCardUM.Empty( + titleReference = resourceReference(id = R.string.swapping_to_title), + subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_receive), + ) +} + +/** + * Convert from [ExchangeCardUM] to [ExchangeCardUM.Filled] + * + * @param selectedTokenItemState token item state + * @param removeButtonUM remove button UI model + */ +internal fun ExchangeCardUM.toFilled( + selectedTokenItemState: TokenItemState, + removeButtonUM: ExchangeCardUM.RemoveButtonUM? = null, +): ExchangeCardUM.Filled { + return ExchangeCardUM.Filled( + titleReference = titleReference, + tokenItemState = selectedTokenItemState, + removeButtonUM = removeButtonUM, + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt new file mode 100644 index 0000000000..40a3b14a91 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt @@ -0,0 +1,79 @@ +package com.tangem.features.onramp.swap.model + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.onramp.component.SwapSelectTokensComponent +import com.tangem.features.onramp.swap.entity.SwapSelectTokensController +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM +import com.tangem.features.onramp.swap.entity.transformer.RemoveSelectedFromTokenTransformer +import com.tangem.features.onramp.swap.entity.transformer.SelectFromTokenTransformer +import com.tangem.features.onramp.swap.entity.transformer.SelectToTokenTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class SwapSelectTokensModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val controller: SwapSelectTokensController, + private val router: Router, +) : Model() { + + val state: StateFlow = controller.state + + val fromCurrencyStatus: StateFlow get() = _fromCurrencyStatus + + private val _fromCurrencyStatus = MutableStateFlow(value = null) + private val _toCurrencyStatus = MutableStateFlow(value = null) + + private val params = paramsContainer.require() + + /** + * Select "from" token + * + * @param selectedTokenItemState selected token item state + * @param status crypto currency status + */ + fun selectFromToken(selectedTokenItemState: TokenItemState, status: CryptoCurrencyStatus) { + _fromCurrencyStatus.value = status + + controller.update( + transformer = SelectFromTokenTransformer( + selectedTokenItemState = selectedTokenItemState, + onRemoveClick = ::onRemoveClick, + ), + ) + } + + /** + * Select "to" token + * + * @param selectedTokenItemState selected token item state + * @param status crypto currency status + */ + fun selectToToken(selectedTokenItemState: TokenItemState, status: CryptoCurrencyStatus) { + _toCurrencyStatus.value = status + + controller.update(transformer = SelectToTokenTransformer(selectedTokenItemState)) + + router.push( + route = AppRoute.Swap( + currencyFrom = requireNotNull(fromCurrencyStatus.value).currency, + currencyTo = status.currency, + userWalletId = params.userWalletId, + ), + ) + } + + private fun onRemoveClick() { + _fromCurrencyStatus.value = null + + controller.update(transformer = RemoveSelectedFromTokenTransformer) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt index 4b1ce4c245..35ad87452f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt @@ -1,13 +1,12 @@ package com.tangem.features.onramp.swap.ui +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable @@ -24,8 +23,10 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.rows.NetworkTitle import com.tangem.core.ui.components.token.TokenItem import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onramp.impl.R @@ -51,34 +52,41 @@ internal fun ExchangeCard(state: ExchangeCardUM, modifier: Modifier = Modifier) ), verticalArrangement = Arrangement.SpaceBetween, ) { - NetworkTitle( - title = { - Text( - text = state.titleReference.resolveReference(), - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography.subtitle2, - ) - }, - action = { - RemoveButton(hasRemoveButton = state.hasRemoveButton, onClick = {}) - }, - ) + Title(titleReference = state.titleReference, removeButtonUM = state.removeButtonUM) - TokenItem(state = state.tokenItemState, isBalanceHidden = false) + AnimatedContent(targetState = state.tokenItemState, label = "TokenItem's changing ") { + TokenItem(state = it, isBalanceHidden = false) + } } } @Composable -private fun RemoveButton(hasRemoveButton: Boolean, onClick: () -> Unit) { - AnimatedVisibility(visible = hasRemoveButton) { +private fun Title(titleReference: TextReference, removeButtonUM: ExchangeCardUM.RemoveButtonUM?) { + NetworkTitle( + title = { + Text( + text = titleReference.resolveReference(), + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.subtitle2, + ) + }, + action = { RemoveButton(state = removeButtonUM) }, + ) +} + +@Composable +private fun RemoveButton(state: ExchangeCardUM.RemoveButtonUM?) { + AnimatedVisibility(visible = state != null) { + state ?: return@AnimatedVisibility + Text( text = stringResource(id = R.string.manage_tokens_remove), modifier = Modifier.clickable( indication = ripple(bounded = false), interactionSource = remember { MutableInteractionSource() }, - onClick = onClick, + onClick = state.onClick, ), color = TangemTheme.colors.text.accent, maxLines = 1, @@ -88,11 +96,17 @@ private fun RemoveButton(hasRemoveButton: Boolean, onClick: () -> Unit) { } } -@Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun Preview_ExchangeCard(@PreviewParameter(ExchangeCardUMProvider::class) state: ExchangeCardUM) { TangemThemePreview { - ExchangeCard(state = state) + ExchangeCard( + state = state, + modifier = Modifier + .background(TangemTheme.colors.background.secondary) + .padding(16.dp), + ) } } @@ -101,14 +115,20 @@ private class ExchangeCardUMProvider : PreviewParameterProvider override val values: Sequence = sequenceOf( ExchangeCardUM.Empty( titleReference = resourceReference(id = R.string.swapping_from_title), - onItemClick = {}, + subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap), ), - ExchangeCardUM.Filled( + createFilled(removeButtonUM = null), + createFilled(removeButtonUM = ExchangeCardUM.RemoveButtonUM { }), + ) + + private fun createFilled(removeButtonUM: ExchangeCardUM.RemoveButtonUM?): ExchangeCardUM.Filled { + return ExchangeCardUM.Filled( titleReference = resourceReference(id = R.string.swapping_from_title), + removeButtonUM = removeButtonUM, tokenItemState = TokenItemState.Content( id = "1", iconState = CurrencyIconState.Locked, - titleState = TokenItemState.TitleState.Content(text = "Bitcoin"), + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"), subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( @@ -119,6 +139,6 @@ private class ExchangeCardUMProvider : PreviewParameterProvider onItemClick = {}, onItemLongClick = {}, ), - ), - ) + ) + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt new file mode 100644 index 0000000000..b2c0e550bd --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt @@ -0,0 +1,101 @@ +package com.tangem.features.onramp.swap.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent +import com.tangem.features.onramp.swap.entity.ExchangeCardUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent + +/** + * Swap select tokens + * + * @param state state + * @param selectFromTokenListComponent select "from" token list component + * @param selectToTokenListComponent select "to" token list component + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun SwapSelectTokens( + state: SwapSelectTokensUM, + selectFromTokenListComponent: OnrampTokenListComponent, + selectToTokenListComponent: AvailableSwapPairsComponent, + modifier: Modifier = Modifier, +) { + BackHandler(onBack = state.onBackClick) + + LazyColumn( + modifier = modifier + .background(TangemTheme.colors.background.secondary) + .imePadding() + .systemBarsPadding(), + contentPadding = PaddingValues(bottom = 8.dp), + ) { + stickyHeader(key = "header") { + AppBarWithBackButton( + onBackClick = state.onBackClick, + text = stringResource(id = R.string.common_swap), + iconRes = R.drawable.ic_close_24, + containerColor = TangemTheme.colors.background.secondary, + ) + } + + item(key = "exchange_from", contentType = "exchange_from") { + ExchangeCard( + state = state.exchangeFrom, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 8.dp, bottom = 12.dp) + .animateItem(), + ) + } + + if (state.exchangeFrom is ExchangeCardUM.Empty) { + item(key = "select_from", contentType = "select_from") { + selectFromTokenListComponent.Content( + modifier = Modifier + .padding(horizontal = 16.dp) + .animateItem(), + ) + } + } + + if (state.exchangeFrom is ExchangeCardUM.Filled) { + item(key = "exchange_to", contentType = "exchange_to") { + ExchangeCard( + state = state.exchangeTo, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp) + .animateItem(), + ) + } + + if (state.exchangeTo is ExchangeCardUM.Empty) { + item(key = "select_to", contentType = "select_to") { + selectToTokenListComponent.Content( + modifier = Modifier + .padding(horizontal = 16.dp) + .animateItem(), + ) + } + } + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt index 67ada9b94b..c538d475cd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt @@ -1,6 +1,5 @@ package com.tangem.features.onramp.tokenlist -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -23,10 +22,10 @@ internal class DefaultOnrampTokenListComponent @AssistedInject constructor( private val model: OnrampTokenListModel = getOrCreateModel(params) @Composable - override fun Content(contentPadding: PaddingValues, modifier: Modifier) { + override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() - TokenList(state = state, contentPadding = contentPadding, modifier = modifier) + TokenList(state = state, modifier = modifier) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt index bcb5ab848a..d85af84cf3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt @@ -1,20 +1,16 @@ package com.tangem.features.onramp.tokenlist -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.ui.Modifier import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.onramp.entity.OnrampOperation +import com.tangem.features.onramp.tokenlist.entity.OnrampOperation /** Token list component that present list of token for multi-currency wallet */ @Stable -internal interface OnrampTokenListComponent { - - @Composable - fun Content(contentPadding: PaddingValues, modifier: Modifier) +internal interface OnrampTokenListComponent : ComposableContentComponent { /** Component factory */ interface Factory : ComponentFactory @@ -31,6 +27,6 @@ internal interface OnrampTokenListComponent { val filterOperation: OnrampOperation, val hasSearchBar: Boolean, val userWalletId: UserWalletId, - val onTokenClick: (CryptoCurrencyStatus) -> Unit, + val onTokenClick: (TokenItemState, CryptoCurrencyStatus) -> Unit, ) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampOperation.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/OnrampOperation.kt similarity index 64% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampOperation.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/OnrampOperation.kt index 1a94304325..dea01060e5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampOperation.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/OnrampOperation.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.entity +package com.tangem.features.onramp.tokenlist.entity /** * Onramp operation @@ -8,5 +8,6 @@ package com.tangem.features.onramp.entity internal enum class OnrampOperation { BUY, SELL, + SWAP, ; } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt index bb9e8f3562..11d24c670b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt @@ -7,27 +7,29 @@ import kotlinx.collections.immutable.persistentListOf /** * Token list UM * - * @property items items (search bar, tokens, headers) - * @property isBalanceHidden flag that indicates if balance should be hidden + * @property availableItems available items (search bar, header, tokens) + * @property unavailableItems unavailable items (header, tokens) + * @property isBalanceHidden flag that indicates if balance should be hidden * [REDACTED_AUTHOR] */ internal data class TokenListUM( - val items: ImmutableList, + val availableItems: ImmutableList, + val unavailableItems: ImmutableList, val isBalanceHidden: Boolean, ) { /** Get search bar if it exists */ fun getSearchBar(): TokensListItemUM.SearchBar? { - return items.firstOrNull() as? TokensListItemUM.SearchBar + return availableItems.firstOrNull() as? TokensListItemUM.SearchBar } /** Get tokens */ fun getTokens(): ImmutableList { - if (getSearchBar() == null) return items + if (getSearchBar() == null) return availableItems - return if (items.size > 1) { - items.subList(fromIndex = 1, toIndex = items.size) + return if (availableItems.size > 1) { + availableItems.subList(fromIndex = 1, toIndex = availableItems.size) } else { persistentListOf() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt index bb662a5b7a..07c86963a5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt @@ -1,6 +1,7 @@ package com.tangem.features.onramp.tokenlist.entity import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.features.onramp.utils.SearchBarUMTransformer import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -19,7 +20,8 @@ internal class TokenListUMController @Inject constructor() { private val _state: MutableStateFlow = MutableStateFlow( value = TokenListUM( - items = persistentListOf(), + availableItems = persistentListOf(), + unavailableItems = persistentListOf(), isBalanceHidden = false, ), ) @@ -34,6 +36,28 @@ internal class TokenListUMController @Inject constructor() { _state.update(transformer::transform) } + fun update(transformer: SearchBarUMTransformer) { + Timber.d("Applying ${transformer::class.simpleName}") + _state.update { prevState -> + val searchBarItem = prevState.getSearchBar() + + if (searchBarItem != null) { + val updatedSearchBar = searchBarItem.copy( + searchBarUM = transformer.transform(searchBarItem.searchBarUM), + ) + + prevState.copy( + availableItems = persistentListOf( + updatedSearchBar, + *prevState.getTokens().toTypedArray(), + ), + ) + } else { + prevState + } + } + } + /** Get search bar if it exists */ fun getSearchBar(): TokensListItemUM.SearchBar? { return _state.value.getSearchBar() diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SearchBarUMTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SearchBarUMTransformer.kt deleted file mode 100644 index db17cb19ea..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SearchBarUMTransformer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.onramp.tokenlist.entity.transformer - -import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.features.onramp.tokenlist.entity.TokenListUM -import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer -import kotlinx.collections.immutable.persistentListOf - -/** - * Base [SearchBarUM] transformer - * -[REDACTED_AUTHOR] - */ -internal abstract class SearchBarUMTransformer : TokenListUMTransformer { - - abstract fun transform(prevState: SearchBarUM): SearchBarUM - - override fun transform(prevState: TokenListUM): TokenListUM { - val searchBarItem = prevState.getSearchBar() - - return if (searchBarItem != null) { - val updatedSearchBar = searchBarItem.copy(searchBarUM = transform(searchBarItem.searchBarUM)) - - prevState.copy( - items = persistentListOf( - updatedSearchBar, - *prevState.getTokens().toTypedArray(), - ), - ) - } else { - prevState - } - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt deleted file mode 100644 index cf69073114..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.features.onramp.tokenlist.entity.transformer - -import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.onramp.impl.R - -internal class UpdateSearchBarActiveStateTransformer(private val isActive: Boolean) : SearchBarUMTransformer() { - - override fun transform(prevState: SearchBarUM): SearchBarUM { - val placeholderText = if (isActive) { - TextReference.EMPTY - } else { - resourceReference(id = R.string.common_search) - } - - return prevState.copy(placeholderText = placeholderText, isActive = isActive) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt index b6065912be..ee219d1fc5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt @@ -1,26 +1,43 @@ package com.tangem.features.onramp.tokenlist.entity.transformer import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import kotlinx.collections.immutable.toImmutableList +@Suppress("LongParameterList") internal class UpdateTokenItemsTransformer( - private val tokenItemStateConverter: TokenItemStateConverter, - private val statuses: List, + private val appCurrency: AppCurrency, + private val onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit, + private val statuses: Map>, private val isBalanceHidden: Boolean, private val hasSearchBar: Boolean, + private val unavailableTokensHeaderReference: TextReference, private val onQueryChange: (String) -> Unit, private val onActiveChange: (Boolean) -> Unit, ) : TokenListUMTransformer { override fun transform(prevState: TokenListUM): TokenListUM { - val items = tokenItemStateConverter.convertList(input = statuses).map(TokensListItemUM::Token) + val availableItems = convertStatuses( + converter = createDefaultTokenItemStateConverter(), + statuses = statuses[true].orEmpty(), + ) + + val unavailableItems = convertStatuses( + converter = createUnavailableTokenItemStateConverter(), + statuses = statuses[false].orEmpty(), + ) val searchBarItem = if (hasSearchBar) { prevState.getSearchBar() ?: createSearchBarItem() @@ -29,11 +46,88 @@ internal class UpdateTokenItemsTransformer( } return prevState.copy( - items = (listOfNotNull(searchBarItem) + items).toImmutableList(), + availableItems = buildList { + if (searchBarItem != null) { + add(searchBarItem) + } + + if (availableItems.isNotEmpty()) { + createGroupTitle( + textReference = resourceReference(id = R.string.exchange_tokens_available_tokens_header), + ) + .let(::add) + } + + addAll(availableItems) + } + .toImmutableList(), + unavailableItems = buildList { + if (unavailableItems.isNotEmpty()) { + createGroupTitle(textReference = unavailableTokensHeaderReference).let(::add) + } + + addAll(unavailableItems) + }.toImmutableList(), isBalanceHidden = isBalanceHidden, ) } + private fun convertStatuses( + converter: TokenItemStateConverter, + statuses: List, + ): List { + return converter.convertList(statuses) + .map(TokensListItemUM::Token) + } + + private fun createDefaultTokenItemStateConverter(): TokenItemStateConverter { + return TokenItemStateConverter(appCurrency = appCurrency, onItemClick = onItemClick) + } + + private fun createUnavailableTokenItemStateConverter(): TokenItemStateConverter { + return TokenItemStateConverter( + appCurrency = appCurrency, + iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) }, + titleStateProvider = { + TokenItemState.TitleState.Content( + text = stringReference(value = it.currency.name), + isAvailable = false, + ) + }, + subtitleStateProvider = { + when (it.value) { + CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading + else -> { + TokenItemState.SubtitleState.TextContent( + value = stringReference(value = it.currency.symbol), + isAvailable = false, + ) + } + } + }, + fiatAmountStateProvider = { + when (it.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> { + TokenItemState.FiatAmountState.TextContent( + text = it.getFormattedFiatAmount(appCurrency), + isAvailable = false, + ) + } + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Loading, + -> null + } + }, + onItemClick = onItemClick, + ) + } + private fun createSearchBarItem(): TokensListItemUM.SearchBar { return TokensListItemUM.SearchBar( searchBarUM = SearchBarUM( @@ -45,4 +139,8 @@ internal class UpdateTokenItemsTransformer( ), ) } + + private fun createGroupTitle(textReference: TextReference): TokensListItemUM.GroupTitle { + return TokensListItemUM.GroupTitle(id = textReference.hashCode(), text = textReference) + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index fc6329296f..ffda60ba6a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -1,9 +1,10 @@ package com.tangem.features.onramp.tokenlist.model import arrow.core.getOrElse -import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -13,14 +14,15 @@ import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.onramp.entity.OnrampOperation +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.tokenlist.entity.OnrampOperation import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMController -import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateSearchBarActiveStateTransformer -import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateSearchQueryTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer -import com.tangem.features.onramp.tokenlist.utils.SearchTokensManager +import com.tangem.features.onramp.utils.SearchManager +import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer +import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -31,7 +33,7 @@ internal class OnrampTokenListModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val tokenListUMController: TokenListUMController, - private val searchTokensManager: SearchTokensManager, + private val searchManager: SearchManager, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getTokenListUseCase: GetTokenListUseCase, @@ -55,7 +57,7 @@ internal class OnrampTokenListModel @Inject constructor( flow = getTokenListUseCase.launch(userWalletId = params.userWalletId).distinctUntilChanged(), flow2 = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(), flow3 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(), - flow4 = searchTokensManager.query, + flow4 = searchManager.query, ) { maybeTokenList, appCurrency, isBalanceHidden, query -> val currencies = maybeTokenList.getOrElse( ifLoading = { it ?: TokenList.Empty }, @@ -68,13 +70,22 @@ internal class OnrampTokenListModel @Inject constructor( .filterByAvailability() UpdateTokenItemsTransformer( - tokenItemStateConverter = TokenItemStateConverter( - appCurrency = appCurrency, - onItemClick = params.onTokenClick, - ), + appCurrency = appCurrency, + onItemClick = params.onTokenClick, statuses = filterTokenList, isBalanceHidden = isBalanceHidden, hasSearchBar = params.hasSearchBar && currencies.isNotEmpty(), + unavailableTokensHeaderReference = when (params.filterOperation) { + OnrampOperation.BUY -> resourceReference(id = R.string.tokens_list_unavailable_to_purchase_header) + OnrampOperation.SELL -> resourceReference(id = R.string.tokens_list_unavailable_to_sell_header) + OnrampOperation.SWAP -> { + // TODO: [REDACTED_JIRA] + resourceReference( + id = R.string.tokens_list_unavailable_to_swap_header, + wrappedList(""), + ) + } + }, onQueryChange = ::onSearchQueryChange, onActiveChange = ::onSearchBarActiveChange, ) @@ -91,12 +102,17 @@ internal class OnrampTokenListModel @Inject constructor( modelScope.launch { tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery)) - searchTokensManager.update(newQuery) + searchManager.update(newQuery) } } private fun onSearchBarActiveChange(isActive: Boolean) { - tokenListUMController.update(transformer = UpdateSearchBarActiveStateTransformer(isActive)) + tokenListUMController.update( + transformer = UpdateSearchBarActiveStateTransformer( + isActive = isActive, + placeHolder = resourceReference(id = R.string.common_search), + ), + ) } private fun List.filterByQuery(query: String): List { @@ -106,14 +122,22 @@ internal class OnrampTokenListModel @Inject constructor( } } - private fun List.filterByAvailability(): List { - return filter { - when (params.filterOperation) { + private suspend fun List.filterByAvailability(): Map> { + return groupBy { status -> + val isAvailable = when (params.filterOperation) { OnrampOperation.BUY -> { - rampStateManager.availableForBuy(scanResponse = scanResponse, cryptoCurrency = it.currency) + rampStateManager.availableForBuy(scanResponse = scanResponse, cryptoCurrency = status.currency) } - OnrampOperation.SELL -> rampStateManager.availableForSell(cryptoCurrency = it.currency) + OnrampOperation.SELL -> rampStateManager.availableForSell(cryptoCurrency = status.currency) + OnrampOperation.SWAP -> rampStateManager.availableForSwap( + userWalletId = params.userWalletId, + cryptoCurrency = status.currency, + ) } + + isAvailable && + status.value !is CryptoCurrencyStatus.MissedDerivation && + status.value !is CryptoCurrencyStatus.Unreachable } } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index c75b080c3b..807100bf6a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -1,21 +1,26 @@ package com.tangem.features.onramp.tokenlist.ui +import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.tokenlist.TokenListItem +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider +import kotlinx.collections.immutable.ImmutableList /** * Token list @@ -26,40 +31,46 @@ import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvide [REDACTED_AUTHOR] */ @Composable -internal fun TokenList(state: TokenListUM, contentPadding: PaddingValues, modifier: Modifier = Modifier) { - LazyColumn(modifier = modifier, contentPadding = contentPadding) { - itemsIndexed( - items = state.items, - key = { _, item -> item.id }, - contentType = { _, item -> item::class.java }, - itemContent = { index, item -> - TokenListItem( - state = item, - isBalanceHidden = state.isBalanceHidden, - modifier = Modifier - .animateItem() - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = state.items.lastIndex, - addDefaultPadding = false, - backgroundColor = TangemTheme.colors.background.primary, - ), - ) - }, - ) +internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { + Column(modifier) { + ItemsBlock(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) + + SpacerH12() + + ItemsBlock(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) } } -@Preview +@Composable +private fun ItemsBlock(items: ImmutableList, isBalanceHidden: Boolean) { + items.fastForEachIndexed { index, item -> + key(item.id) { + TokenListItem( + state = item, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + addDefaultPadding = false, + backgroundColor = TangemTheme.colors.background.primary, + ), + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun Preview_TokenList(@PreviewParameter(PreviewTokenListUMProvider::class) state: TokenListUM) { TangemThemePreview { TokenList( state = state, - contentPadding = PaddingValues(all = 16.dp), modifier = Modifier .fillMaxWidth() - .background(color = TangemTheme.colors.background.secondary), + .background(color = TangemTheme.colors.background.secondary) + .padding(16.dp), ) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt index ce2f3e4f88..501874e719 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt @@ -7,20 +7,32 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList internal class PreviewTokenListUMProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( - createTokensList(hasSearchBar = false, createDefaultTokenItem()), - createTokensList(hasSearchBar = true, createDefaultTokenItem()), + createTokensList( + hasSearchBar = false, + createHeader(), + createDefaultTokenItem(), + createDefaultTokenItem(), + ), + createTokensList( + hasSearchBar = true, + createHeader(), + createDefaultTokenItem(), + createDefaultTokenItem(), + ), ) private fun createTokensList(hasSearchBar: Boolean, vararg items: TokensListItemUM): TokenListUM { return TokenListUM( - items = buildList { + availableItems = buildList { if (hasSearchBar) { TokensListItemUM.SearchBar( searchBarUM = SearchBarUM( @@ -37,16 +49,28 @@ internal class PreviewTokenListUMProvider : PreviewParameterProvider { + + abstract override fun transform(prevState: SearchBarUM): SearchBarUM +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/utils/SearchTokensManager.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/SearchManager.kt similarity index 84% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/utils/SearchTokensManager.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/SearchManager.kt index dde1a1ab4f..a2f80e0f27 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/utils/SearchTokensManager.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/SearchManager.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.tokenlist.utils +package com.tangem.features.onramp.utils import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.withDebounce @@ -12,7 +12,7 @@ import javax.inject.Inject * [REDACTED_AUTHOR] */ -internal class SearchTokensManager @Inject constructor() { +internal class SearchManager @Inject constructor() { val query: Flow get() = _query @@ -24,6 +24,7 @@ internal class SearchTokensManager @Inject constructor() { suspend fun update(value: String) { coroutineScope { if (value.isEmpty()) { + jobHolder.cancel() _query.value = value } else { withDebounce(jobHolder) { _query.value = value } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/UpdateSearchBarActiveStateTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/UpdateSearchBarActiveStateTransformer.kt new file mode 100644 index 0000000000..c2727a443d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/UpdateSearchBarActiveStateTransformer.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onramp.utils + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.TextReference + +internal class UpdateSearchBarActiveStateTransformer( + private val isActive: Boolean, + private val placeHolder: TextReference, +) : SearchBarUMTransformer() { + + override fun transform(prevState: SearchBarUM): SearchBarUM { + return prevState.copy(placeholderText = if (isActive) TextReference.EMPTY else placeHolder, isActive = isActive) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/UpdateSearchQueryTransformer.kt similarity index 81% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/UpdateSearchQueryTransformer.kt index 3e463d159a..961fa9cbe1 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/UpdateSearchQueryTransformer.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.tokenlist.entity.transformer +package com.tangem.features.onramp.utils import com.tangem.core.ui.components.fields.entity.SearchBarUM diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/featuretoggles/StakingFeatureToggles.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/featuretoggles/StakingFeatureToggles.kt deleted file mode 100644 index 90ccdc617d..0000000000 --- a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/featuretoggles/StakingFeatureToggles.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.staking.api.featuretoggles - -/** - * Staking feature toggles - */ -interface StakingFeatureToggles { - - /** Availability of staking */ - val isStakingEnabled: Boolean -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingFeatureTogglesModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingFeatureTogglesModule.kt deleted file mode 100644 index e40072f8bf..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingFeatureTogglesModule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.features.staking.impl.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles -import com.tangem.features.staking.impl.featuretoggles.DefaultStakingFeatureToggles -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -/** - * DI module provides implementation of [StakingFeatureToggles] - */ -@Module -@InstallIn(SingletonComponent::class) -internal object StakingFeatureTogglesModule { - - @Provides - @Singleton - fun provideStakingFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles { - return DefaultStakingFeatureToggles(featureTogglesManager = featureTogglesManager) - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/featuretoggles/DefaultStakingFeatureToggles.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/featuretoggles/DefaultStakingFeatureToggles.kt deleted file mode 100644 index 735c22b027..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/featuretoggles/DefaultStakingFeatureToggles.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.features.staking.impl.featuretoggles - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles - -/** - * Default implementation of staking feature toggles - * - * @property featureTogglesManager manager for getting information about the availability of feature toggles - */ -internal class DefaultStakingFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : StakingFeatureToggles { - - override val isStakingEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_ENABLED") -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 4150cef942..e66c38a778 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -63,7 +63,7 @@ internal class StakingStateController @Inject constructor( } } - private fun dismissAlert() { + fun dismissAlert() { mutableUiState.update { it.copy(event = consumedEvent()) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt index a00130dcab..3d384541bd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt @@ -33,4 +33,12 @@ internal sealed class StakingAlertUM : AlertUM { override val confirmButtonText = resourceReference(R.string.common_ok) override val onConfirmClick = null } + + data class FeeIncreased( + override val onConfirmClick: () -> Unit, + ) : StakingAlertUM() { + override val title: TextReference? = null + override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt index 3ccad868cc..09b6415c41 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -4,9 +4,14 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.domain.staking.* +import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase +import com.tangem.domain.staking.GetStakingTransactionUseCase +import com.tangem.domain.staking.SaveUnsubmittedHashUseCase +import com.tangem.domain.staking.SubmitHashUseCase import com.tangem.domain.staking.model.SubmitHashData -import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction @@ -18,7 +23,10 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingStateController +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions import com.tangem.utils.extensions.orZero @@ -55,6 +63,7 @@ internal class StakingTransactionSender @AssistedInject constructor( onConstructError: (StakingError) -> Unit, onSendSuccess: (String) -> Unit, onSendError: (SendTransactionError?) -> Unit, + onFeeIncreased: (Fee) -> Unit, ) { val state = stateController.value @@ -82,13 +91,22 @@ internal class StakingTransactionSender @AssistedInject constructor( return } - onConstructSuccess(fullTransactionsData.map { it.stakeKitTransaction }) + val totalFee = fullTransactionsData.sumOf { it.stakeKitTransaction.gasEstimate?.amount.orZero() } - sendStakingTransaction( - fullTransactionsData = fullTransactionsData, - onSendSuccess = onSendSuccess, - onSendError = onSendError, - ) + if (fee.amount.value.orZero() >= totalFee) { + onConstructSuccess(fullTransactionsData.map { it.stakeKitTransaction }) + sendStakingTransaction( + fullTransactionsData = fullTransactionsData, + onSendSuccess = onSendSuccess, + onSendError = onSendError, + ) + } else { + onFeeIncreased( + Fee.Common( + fee.amount.copy(value = totalFee), + ), + ) + } } private suspend fun getStakingTransactions( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt index 9043cc8ae9..c35018c9e7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt @@ -1,6 +1,5 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.core.ui.extensions.TextReference import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState @@ -19,7 +18,6 @@ internal class SetConfirmationStateInProgressTransformer : Transformer + stateController.updateAll( + SetConfirmationStateResetAssentTransformer, + SetConfirmationStateAssentTransformer( + appCurrencyProvider = Provider { appCurrency }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fee = increasedFee, + ), + ) + stateController.updateEvent( + StakingEvent.ShowAlert( + StakingAlertUM.FeeIncreased(stateController::dismissAlert), + ), + ) + updateNotifications() + }, ) }.saveIn(sendTransactionJobHolder) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/GetAvailablePairsUseCase.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/GetAvailablePairsUseCase.kt new file mode 100644 index 0000000000..113c0ca915 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/GetAvailablePairsUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.swap.domain + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo +import com.tangem.feature.swap.domain.models.domain.SwapPairLeast + +class GetAvailablePairsUseCase( + private val swapRepository: SwapRepository, +) { + + suspend operator fun invoke( + initialCurrency: LeastTokenInfo, + currencies: List, + ): List { + return swapRepository.getPairsOnly(initialCurrency, currencies).pairs + } +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 1d5e225d91..4072c1736a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1047,6 +1047,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( CoreTestnet, Xodex, Canxium, + Chiliz, + ChilizTestnet, -> Fee.Common(feeAmount) // endregion } diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 3b0dbe2d6d..84cbe75297 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.features.swap.domain) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) + implementation(projects.features.wallet.api) /** AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index e841fb0bba..19a49d15b2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -63,9 +63,13 @@ internal class StateBuilder( appCurrencyProvider = appCurrencyProvider, ) - fun createInitialLoadingState(initialCurrency: CryptoCurrency, networkInfo: NetworkInfo): SwapStateHolder { + fun createInitialLoadingState( + initialCurrencyFrom: CryptoCurrency, + initialCurrencyTo: CryptoCurrency?, + fromNetworkInfo: NetworkInfo, + ): SwapStateHolder { return SwapStateHolder( - blockchainId = networkInfo.blockchainId, + blockchainId = fromNetworkInfo.blockchainId, sendCardData = SwapCardState.SwapCardData( type = TransactionCardType.Inputtable( onAmountChanged = actions.onAmountChanged, @@ -75,27 +79,27 @@ internal class StateBuilder( amountEquivalent = null, amountTextFieldValue = null, token = null, - tokenIconUrl = initialCurrency.iconUrl, - tokenCurrency = initialCurrency.symbol, - coinId = initialCurrency.network.backendId, + tokenIconUrl = initialCurrencyFrom.iconUrl, + tokenCurrency = initialCurrencyFrom.symbol, + coinId = initialCurrencyFrom.network.backendId, canSelectAnotherToken = false, - isNotNativeToken = initialCurrency is CryptoCurrency.Token, + isNotNativeToken = initialCurrencyFrom is CryptoCurrency.Token, balance = "", - networkIconRes = getActiveIconRes(initialCurrency.network.id.value), + networkIconRes = getActiveIconRes(initialCurrencyFrom.network.id.value), isBalanceHidden = true, ), receiveCardData = SwapCardState.SwapCardData( type = TransactionCardType.ReadOnly(), amountEquivalent = null, - tokenIconUrl = "", - tokenCurrency = "", + tokenIconUrl = initialCurrencyTo?.iconUrl, + tokenCurrency = initialCurrencyTo?.symbol ?: "", token = null, amountTextFieldValue = null, canSelectAnotherToken = false, balance = "", - isNotNativeToken = false, - networkIconRes = null, - coinId = null, + isNotNativeToken = initialCurrencyTo is CryptoCurrency.Token, + networkIconRes = initialCurrencyTo?.let { getActiveIconRes(it.network.id.value) }, + coinId = initialCurrencyTo?.network?.backendId, isBalanceHidden = true, ), fee = FeeItemState.Empty, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 216f2860fa..3aa4b23a82 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -50,6 +50,7 @@ import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero @@ -85,13 +86,26 @@ internal class SwapViewModel @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, + private val walletFeatureToggles: WalletFeatureToggles, swapInteractorFactory: SwapInteractor.Factory, - savedStateHandle: SavedStateHandle, + private val savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { - private val initialCryptoCurrency: CryptoCurrency = savedStateHandle.get(AppRoute.Swap.CURRENCY_BUNDLE_KEY) - ?.unbundle(CryptoCurrency.serializer()) - ?: error("no expected parameter CryptoCurrency found") + private val initialCurrencyFrom: CryptoCurrency + get() { + return savedStateHandle.get(AppRoute.Swap.CURRENCY_FROM_KEY) + ?.unbundle(CryptoCurrency.serializer()) + ?: error("no expected parameter CryptoCurrency (from) found") + } + + private val initialCurrencyTo: CryptoCurrency? + get() = if (walletFeatureToggles.isMainActionButtonsEnabled) { + savedStateHandle.get(AppRoute.Swap.CURRENCY_TO_KEY) + ?.unbundle(CryptoCurrency.serializer()) + ?: error("no expected parameter CryptoCurrency (to) found") + } else { + null + } private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Swap.USER_WALLET_ID_KEY) ?.unbundle(UserWalletId.serializer()) @@ -102,7 +116,8 @@ internal class SwapViewModel @Inject constructor( private val swapInteractor = swapInteractorFactory.create(userWalletId) - private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus + private lateinit var initialFromStatus: CryptoCurrencyStatus + private var initialToStatus: CryptoCurrencyStatus? = null private var userWallet: UserWallet by Delegates.notNull() private var isBalanceHidden = true @@ -124,8 +139,9 @@ internal class SwapViewModel @Inject constructor( var uiState: SwapStateHolder by mutableStateOf( stateBuilder.createInitialLoadingState( - initialCurrency = initialCryptoCurrency, - networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId), + initialCurrencyFrom = initialCurrencyFrom, + initialCurrencyTo = initialCurrencyTo, + fromNetworkInfo = blockchainInteractor.getBlockchainInfo(initialCurrencyFrom.network.backendId), ), ) private set @@ -153,14 +169,16 @@ internal class SwapViewModel @Inject constructor( init { viewModelScope.launch(dispatchers.io) { - val cryptoCurrencyStatus = - getCryptoCurrencyStatusUseCase(userWalletId, initialCryptoCurrency.id).getOrNull() + val fromStatus = getCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom.id).getOrNull() + val toStatus = initialCurrencyTo?.let { getCryptoCurrencyStatusUseCase(userWalletId, it.id).getOrNull() } val wallet = getUserWalletUseCase(userWalletId).getOrNull() - if (cryptoCurrencyStatus == null || wallet == null) { + val isStatusToNull = walletFeatureToggles.isMainActionButtonsEnabled && toStatus == null + if (fromStatus == null || wallet == null || isStatusToNull) { uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) } else { userWallet = wallet - initialCryptoCurrencyStatus = cryptoCurrencyStatus + initialFromStatus = fromStatus + initialToStatus = toStatus initTokens(isInitiallyReversed) } } @@ -181,7 +199,7 @@ internal class SwapViewModel @Inject constructor( } fun onScreenOpened() { - analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCryptoCurrency.symbol)) + analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCurrencyFrom.symbol)) } fun setRouter(router: SwapRouter) { @@ -207,14 +225,19 @@ internal class SwapViewModel @Inject constructor( private fun initTokens(isReverseFromTo: Boolean) { viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { - swapInteractor.getTokensDataState(initialCryptoCurrency) + swapInteractor.getTokensDataState(initialCurrencyFrom) }.onSuccess { state -> updateTokensState(state) - val selectedCurrency = swapInteractor.getInitialCurrencyToSwap( - initialCryptoCurrency = initialCryptoCurrency, - state = state, - isReverseFromTo = isReverseFromTo, - ) + val selectedCurrency = if (walletFeatureToggles.isMainActionButtonsEnabled) { + initialToStatus + } else { + swapInteractor.getInitialCurrencyToSwap( + initialCryptoCurrency = initialCurrencyFrom, + state = state, + isReverseFromTo = isReverseFromTo, + ) + } + applyInitialTokenChoice( state = state, selectedCurrency = selectedCurrency, @@ -250,9 +273,10 @@ internal class SwapViewModel @Inject constructor( (it as? ExpressException)?.expressDataError?.code ?: ExpressDataError.UnknownError.code, ) { uiState = stateBuilder.createInitialLoadingState( - initialCurrency = initialCryptoCurrency, - networkInfo = blockchainInteractor.getBlockchainInfo( - initialCryptoCurrency.network.backendId, + initialCurrencyFrom = initialCurrencyFrom, + initialCurrencyTo = initialCurrencyTo, + fromNetworkInfo = blockchainInteractor.getBlockchainInfo( + initialCurrencyFrom.network.backendId, ), ) initTokens(isReverseFromTo) @@ -271,15 +295,15 @@ internal class SwapViewModel @Inject constructor( analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap) uiState = stateBuilder.createNoAvailableTokensToSwapState( uiStateHolder = uiState, - fromToken = initialCryptoCurrencyStatus, + fromToken = initialFromStatus, ) return } isOrderReversed = isReverseFromTo val (fromCurrencyStatus, toCurrencyStatus) = if (isOrderReversed) { - selectedCurrency to initialCryptoCurrencyStatus + selectedCurrency to initialFromStatus } else { - initialCryptoCurrencyStatus to selectedCurrency + initialFromStatus to selectedCurrency } dataState = dataState.copy( fromCryptoCurrency = fromCurrencyStatus, @@ -299,7 +323,7 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.addTokensToState( uiState = uiState, tokensDataState = tokensDataState, - fromToken = dataState.fromCryptoCurrency?.currency ?: initialCryptoCurrency, + fromToken = dataState.fromCryptoCurrency?.currency ?: initialCurrencyFrom, ) } @@ -316,7 +340,7 @@ internal class SwapViewModel @Inject constructor( uiState, fromToken.currency, toToken.currency, - initialCryptoCurrency.id.value, + initialCurrencyFrom.id.value, ) } singleTaskScheduler.scheduleTask( @@ -412,7 +436,7 @@ internal class SwapViewModel @Inject constructor( if (uiState.warnings.any { it is SwapWarning.UnableToCoverFeeWarning }) { analyticsEventHandler.send( SwapEvents.NoticeNotEnoughFee( - token = initialCryptoCurrency.symbol, + token = initialCurrencyFrom.symbol, blockchain = fromToken.currency.network.name, ), ) @@ -450,7 +474,7 @@ internal class SwapViewModel @Inject constructor( } analyticsEventHandler.send( SwapEvents.NoticeProviderError( - sendToken = "${initialCryptoCurrency.network.backendId}:${initialCryptoCurrency.symbol}", + sendToken = "${initialCurrencyFrom.network.backendId}:${initialCurrencyFrom.symbol}", receiveToken = receiveToken ?: "", provider = provider, errorCode = error.code, @@ -588,7 +612,7 @@ internal class SwapViewModel @Inject constructor( swapRouter.openUrl(url) } analyticsEventHandler.send( - event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol), + event = SwapEvents.ButtonExplore(initialCurrencyFrom.symbol), ) }, onStatusClick = { @@ -596,7 +620,7 @@ internal class SwapViewModel @Inject constructor( if (!txExternalUrl.isNullOrBlank()) { swapRouter.openUrl(txExternalUrl) analyticsEventHandler.send( - event = SwapEvents.ButtonStatus(initialCryptoCurrency.symbol), + event = SwapEvents.ButtonStatus(initialCurrencyFrom.symbol), ) } }, @@ -767,7 +791,7 @@ internal class SwapViewModel @Inject constructor( val toToken: CryptoCurrencyStatus if (isOrderReversed) { fromToken = foundToken.currencyStatus - toToken = initialCryptoCurrencyStatus + toToken = initialFromStatus val newToken = fromToken.currency as? CryptoCurrency.Coin if (newToken != null) { @@ -778,7 +802,7 @@ internal class SwapViewModel @Inject constructor( ) } } else { - fromToken = initialCryptoCurrencyStatus + fromToken = initialFromStatus toToken = foundToken.currencyStatus val newToken = toToken.currency as? CryptoCurrency.Coin @@ -1284,7 +1308,7 @@ internal class SwapViewModel @Inject constructor( private fun onFailedTxEmailClick(errorMessage: String) { viewModelScope.launch { - val network = initialCryptoCurrencyStatus.currency.network + val network = initialFromStatus.currency.network val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") } saveBlockchainErrorUseCase( @@ -1293,7 +1317,7 @@ internal class SwapViewModel @Inject constructor( blockchainId = network.id.value, derivationPath = network.derivationPath.value, destinationAddress = dataState.swapDataModel?.transaction?.txTo.orEmpty(), - tokenSymbol = initialCryptoCurrency.symbol, + tokenSymbol = initialCurrencyFrom.symbol, amount = dataState.amount.orEmpty(), fee = dataState.selectedFee?.feeCryptoFormatted.orEmpty(), ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index e9d2706803..a8bf99a969 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -23,7 +23,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.lib.crypto.BlockchainUtils.isBSC import com.tangem.lib.crypto.BlockchainUtils.isSolana @@ -44,7 +43,6 @@ internal class TokenDetailsLoadedBalanceConverter( private val symbol: String, private val decimals: Int, private val clickIntents: TokenDetailsClickIntents, - private val stakingFeatureToggles: StakingFeatureToggles, ) : Converter, TokenDetailsState> { private val txHistoryItemConverter by lazy { @@ -98,7 +96,7 @@ internal class TokenDetailsLoadedBalanceConverter( ): TokenDetailsBalanceBlockState { val stakingCryptoAmount = (status.value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance() val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } - val isBalanceSelectorEnabled = stakingFeatureToggles.isStakingEnabled && !stakingCryptoAmount.isNullOrZero() + val isBalanceSelectorEnabled = !stakingCryptoAmount.isNullOrZero() return when (status.value) { is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.Loaded, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index c3bc29d4a4..23aa2143f1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -38,7 +38,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.t import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider import kotlinx.collections.immutable.toImmutableList @@ -56,7 +55,6 @@ internal class TokenDetailsStateFactory( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, - stakingFeatureToggles: StakingFeatureToggles, getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, symbol: String, decimals: Int, @@ -85,7 +83,6 @@ internal class TokenDetailsStateFactory( symbol = symbol, decimals = decimals, clickIntents = clickIntents, - stakingFeatureToggles = stakingFeatureToggles, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 34c9d9f591..a83d17ed6f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -71,7 +71,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -109,7 +108,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase, private val getStakingEntryInfoUseCase: GetStakingEntryInfoUseCase, - private val stakingFeatureToggles: StakingFeatureToggles, private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, private val getYieldUseCase: GetYieldUseCase, private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, @@ -164,7 +162,6 @@ internal class TokenDetailsViewModel @Inject constructor( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - stakingFeatureToggles = stakingFeatureToggles, getStakingIntegrationIdUseCase = getStakingIntegrationIdUseCase, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, @@ -236,9 +233,7 @@ internal class TokenDetailsViewModel @Inject constructor( subscribeOnExchangeTransactionsUpdates() updateTxHistory(refresh = false, showItemsLoading = true) - if (stakingFeatureToggles.isStakingEnabled) { - updateStakingInfo() - } + updateStakingInfo() } private fun handleBalanceHiding(owner: LifecycleOwner) { @@ -622,7 +617,7 @@ internal class TokenDetailsViewModel @Inject constructor( if (handleUnavailabilityReason(unavailabilityReason)) return - appRouter.push(AppRoute.Swap(currency = cryptoCurrency, userWalletId = userWalletId)) + appRouter.push(AppRoute.Swap(currencyFrom = cryptoCurrency, userWalletId = userWalletId)) } override fun onDismissDialog() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index aab1754f14..99a762cea6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.impl.R @@ -76,7 +77,7 @@ internal object WalletPreviewData { isGrayscale = false, showCustomBadge = false, ), - titleState = TokenItemState.TitleState.Content(text = "Polygon"), + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"), ) } @@ -108,7 +109,7 @@ internal object WalletPreviewData { tokenItemState = tokenItemDragState.copy( id = "${group.id}_token_$tokenNumber", titleState = TokenItemState.TitleState.Content( - text = "Token $tokenNumber from $networkNumber network", + text = stringReference(value = "Token $tokenNumber from $networkNumber network"), ), ), groupId = group.id, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index a871134063..8082bd05e2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -19,7 +19,7 @@ internal object WalletScreenPreviewData { private val tokenItemState = TokenItemState.Content( id = "1", iconState = CurrencyIconState.Locked, - titleState = TokenItemState.TitleState.Content(text = "Bitcoin"), + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"), subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( @@ -45,7 +45,7 @@ internal object WalletScreenPreviewData { TokensListItemUM.Token( state = tokenItemState.copy( id = "2", - titleState = TokenItemState.TitleState.Content(text = "Ethereum"), + titleState = TokenItemState.TitleState.Content(text = stringReference("Ethereum")), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"), subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "1,856660295 ETH"), subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( @@ -59,7 +59,7 @@ internal object WalletScreenPreviewData { state = TokenItemState.Unreachable( id = "3", iconState = CurrencyIconState.Locked, - titleState = TokenItemState.TitleState.Content(text = "Polygon"), + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), onItemClick = {}, onItemLongClick = {}, ), @@ -67,7 +67,7 @@ internal object WalletScreenPreviewData { TokensListItemUM.Token( state = tokenItemState.copy( id = "4", - titleState = TokenItemState.TitleState.Content(text = "Shiba Inu"), + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Shiba Inu")), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"), subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "6 200 220,00 SHIB"), subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( @@ -119,6 +119,7 @@ internal object WalletScreenPreviewData { onRefresh = {}, ), walletCardState = miltiUnreachableCard, + buttons = persistentListOf(buyButton), warnings = persistentListOf( WalletNotification.Warning.SomeNetworksUnreachable, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index caf49acdd4..ecc6383e95 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.it import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -46,7 +47,7 @@ internal class CryptoCurrencyToDraggableItemConverter( return TokenItemState.Draggable( id = getTokenItemId(currency.id), iconState = iconStateConverter.convert(currencyStatus), - titleState = TokenItemState.TitleState.Content(text = currency.name), + titleState = TokenItemState.TitleState.Content(text = stringReference(currency.name)), subtitle2State = if (currencyStatus.value.isError) { TokenItemState.Subtitle2State.Unreachable } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index ee5cdac9f1..9a11997960 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet @@ -10,10 +11,12 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletActionsSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles @Suppress("LongParameterList") internal class MultiWalletContentLoader( @@ -28,10 +31,12 @@ internal class MultiWalletContentLoader( private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, + private val rampStateManager: RampStateManager, + private val walletFeatureToggles: WalletFeatureToggles, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { - return listOf( + return listOfNotNull( MultiWalletTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -50,6 +55,16 @@ internal class MultiWalletContentLoader( getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, ), + createMultiWalletButtonsSubscriber(), ) } + + private fun createMultiWalletButtonsSubscriber(): MultiWalletActionsSubscriber? { + return MultiWalletActionsSubscriber( + userWallet = userWallet, + rampStateManager = rampStateManager, + stateController = stateHolder, + ) + .takeIf { walletFeatureToggles.isMainActionButtonsEnabled } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 7a70b865b2..5bc11a8b00 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet @@ -11,6 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import dagger.hilt.android.scopes.ViewModelScoped import javax.inject.Inject @@ -26,6 +28,8 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, + private val rampStateManager: RampStateManager, + private val walletFeatureToggles: WalletFeatureToggles, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { @@ -41,6 +45,8 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, applyTokenListSortingUseCase = applyTokenListSortingUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, + rampStateManager = rampStateManager, + walletFeatureToggles = walletFeatureToggles, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt index 4c811a13ef..515041aec5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt @@ -38,13 +38,16 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { override val enabled: Boolean, override val dimContent: Boolean, override val onClick: () -> Unit, + val isInProgress: Boolean = false, ) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_buy), iconResId = R.drawable.ic_plus_24, onClick = onClick, + enabled = enabled, dimContent = dimContent, + isInProgress = isInProgress, ), ) @@ -64,6 +67,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { text = TextReference.Res(id = R.string.common_send), iconResId = R.drawable.ic_arrow_up_24, onClick = onClick, + enabled = enabled, dimContent = dimContent, ), ) @@ -83,6 +87,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_receive), iconResId = R.drawable.ic_arrow_down_24, + enabled = enabled, dimContent = dimContent, onClick = onClick, onLongClick = onLongClick, @@ -98,6 +103,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { text = TextReference.Res(id = R.string.common_stake), iconResId = R.drawable.ic_staking_24, onClick = onClick, + enabled = enabled, dimContent = dimContent, ), ) @@ -113,12 +119,15 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { override val enabled: Boolean, override val dimContent: Boolean, override val onClick: () -> Unit, + val isInProgress: Boolean = false, ) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_sell), iconResId = R.drawable.ic_currency_24, onClick = onClick, + enabled = enabled, dimContent = dimContent, + isInProgress = isInProgress, ), ) @@ -133,12 +142,15 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { override val enabled: Boolean, override val dimContent: Boolean, override val onClick: () -> Unit, + val isInProgress: Boolean = false, ) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.swapping_swap_action), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, + enabled = enabled, dimContent = dimContent, + isInProgress = isInProgress, ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 748fb7136c..6a23ccc059 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -24,6 +24,7 @@ internal sealed interface WalletState : WalletStateHolder { data class Content( override val pullToRefreshConfig: PullToRefreshConfig, override val walletCardState: WalletCardState, + override val buttons: PersistentList, override val warnings: ImmutableList, override val bottomSheetConfig: TangemBottomSheetConfig?, override val tokensListState: WalletTokensListState, @@ -31,13 +32,15 @@ internal sealed interface WalletState : WalletStateHolder { data class Locked( override val walletCardState: WalletCardState, + override val buttons: PersistentList, override val bottomSheetConfig: TangemBottomSheetConfig?, val onUnlockNotificationClick: () -> Unit, ) : MultiCurrency(), WalletStateHolder by LockedWalletStateHolder( - walletCardState, - bottomSheetConfig, - onUnlockNotificationClick, + walletCardState = walletCardState, + buttons = buttons, + bottomSheetConfig = bottomSheetConfig, + onUnlockNotificationClick = onUnlockNotificationClick, ) { override val tokensListState = WalletTokensListState.ContentState.Locked @@ -46,7 +49,6 @@ internal sealed interface WalletState : WalletStateHolder { sealed class SingleCurrency : WalletState, TxHistoryStateHolder { - abstract val buttons: PersistentList abstract val marketPriceBlockState: MarketPriceBlockState? data class Content( @@ -68,9 +70,10 @@ internal sealed interface WalletState : WalletStateHolder { ) : SingleCurrency(), TxHistoryStateHolder by LockedTxHistoryStateHolder(onExploreClick), WalletStateHolder by LockedWalletStateHolder( - walletCardState, - bottomSheetConfig, - onUnlockNotificationClick, + walletCardState = walletCardState, + buttons = buttons, + bottomSheetConfig = bottomSheetConfig, + onUnlockNotificationClick = onUnlockNotificationClick, ) { override val marketPriceBlockState: MarketPriceBlockState? = null @@ -84,6 +87,7 @@ internal sealed interface WalletState : WalletStateHolder { data class Content( override val pullToRefreshConfig: PullToRefreshConfig, override val walletCardState: WalletCardState, + override val buttons: PersistentList, override val warnings: ImmutableList, override val bottomSheetConfig: TangemBottomSheetConfig?, override val balancesAndLimitBlockState: BalancesAndLimitsBlockState, @@ -93,15 +97,17 @@ internal sealed interface WalletState : WalletStateHolder { data class Locked( override val walletCardState: WalletCardState, - val onUnlockNotificationClick: () -> Unit, + override val buttons: PersistentList, override val bottomSheetConfig: TangemBottomSheetConfig?, + val onUnlockNotificationClick: () -> Unit, val onExploreClick: () -> Unit, ) : Visa(), TxHistoryStateHolder by LockedTxHistoryStateHolder(onExploreClick), WalletStateHolder by LockedWalletStateHolder( - walletCardState, - bottomSheetConfig, - onUnlockNotificationClick, + walletCardState = walletCardState, + buttons = buttons, + bottomSheetConfig = bottomSheetConfig, + onUnlockNotificationClick = onUnlockNotificationClick, ) { override val balancesAndLimitBlockState: BalancesAndLimitsBlockState? = null diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt index 518723c898..4211ef693c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt @@ -3,20 +3,24 @@ package com.tangem.feature.wallet.presentation.wallet.state.model.holder import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf internal interface WalletStateHolder { val pullToRefreshConfig: PullToRefreshConfig val walletCardState: WalletCardState + val buttons: PersistentList val warnings: ImmutableList val bottomSheetConfig: TangemBottomSheetConfig? } internal class LockedWalletStateHolder( override val walletCardState: WalletCardState, + override val buttons: PersistentList, override val bottomSheetConfig: TangemBottomSheetConfig?, onUnlockNotificationClick: () -> Unit, ) : WalletStateHolder { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt index 3f2da0d9a7..86e534351d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt @@ -5,18 +5,21 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.toImmutableList internal class AddWalletTransformer( private val userWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, + walletFeatureToggles = walletFeatureToggles, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index 96d978a00f..0c85aed9cd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -7,6 +7,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.presentation.wallet.state.utils.createStateByWalletType import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -16,10 +17,15 @@ internal class InitializeWalletsTransformer( private val wallets: List, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { - WalletLoadingStateFactory(clickIntents = clickIntents, walletImageResolver = walletImageResolver) + WalletLoadingStateFactory( + clickIntents = clickIntents, + walletImageResolver = walletImageResolver, + walletFeatureToggles = walletFeatureToggles, + ) } override fun transform(prevState: WalletScreenState): WalletScreenState { @@ -52,6 +58,7 @@ internal class InitializeWalletsTransformer( multiCurrencyCreator = { WalletState.MultiCurrency.Locked( walletCardState = userWallet.toLockedWalletCardState(), + buttons = createMultiWalletDisabledButtons(), bottomSheetConfig = null, onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, ) @@ -68,6 +75,7 @@ internal class InitializeWalletsTransformer( visaWalletCreator = { WalletState.Visa.Locked( walletCardState = userWallet.toLockedWalletCardState(), + buttons = createMultiWalletDisabledButtons(), bottomSheetConfig = null, onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, onExploreClick = clickIntents::onExploreClick, @@ -87,6 +95,17 @@ internal class InitializeWalletsTransformer( ) } + private fun createMultiWalletDisabledButtons(): PersistentList { + if (!walletFeatureToggles.isMainActionButtonsEnabled) return persistentListOf() + + return persistentListOf( + WalletManageButton.Receive(enabled = false, dimContent = false, onClick = {}, onLongClick = null), + WalletManageButton.Send(enabled = false, dimContent = false, onClick = {}), + WalletManageButton.Buy(enabled = false, dimContent = false, onClick = {}), + WalletManageButton.Sell(enabled = false, dimContent = false, onClick = {}), + ) + } + private fun createDisabledButtons(): PersistentList { return persistentListOf( WalletManageButton.Receive(enabled = false, dimContent = false, onClick = {}, onLongClick = null), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index f083b489f0..45e0d62979 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.toImmutableList /** @@ -22,10 +23,15 @@ internal class ReinitializeWalletTransformer( private val newUserWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { - WalletLoadingStateFactory(clickIntents = clickIntents, walletImageResolver = walletImageResolver) + WalletLoadingStateFactory( + clickIntents = clickIntents, + walletImageResolver = walletImageResolver, + walletFeatureToggles = walletFeatureToggles, + ) } override fun transform(prevState: WalletScreenState): WalletScreenState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 7da90fd1ec..8d4c837f82 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -7,6 +7,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenSta import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.toImmutableList import timber.log.Timber @@ -14,10 +15,15 @@ internal class UnlockWalletTransformer( private val unlockedWallets: List, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { - WalletLoadingStateFactory(clickIntents = clickIntents, walletImageResolver = walletImageResolver) + WalletLoadingStateFactory( + clickIntents = clickIntents, + walletImageResolver = walletImageResolver, + walletFeatureToggles = walletFeatureToggles, + ) } override fun transform(prevState: WalletScreenState): WalletScreenState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionsTransformer.kt new file mode 100644 index 0000000000..a59a4bf108 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionsTransformer.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import timber.log.Timber + +internal class UpdateMultiWalletActionsTransformer( + userWalletId: UserWalletId, + private val buyStatus: Lce, + private val sellStatus: Lce, + private val swapStatus: Lce, +) : WalletStateTransformer(userWalletId = userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(buttons = prevState.buttons.updateButtons()) + } + is WalletState.MultiCurrency.Locked -> { + Timber.w("Impossible to load primary currency status for locked wallet") + prevState + } + is WalletState.SingleCurrency -> { + Timber.w("Impossible to load crypto currency actions for multi-currency wallet") + prevState + } + is WalletState.Visa -> { + Timber.w("Impossible to load crypto currency actions for VISA wallet") + prevState + } + } + } + + private fun PersistentList.updateButtons(): PersistentList { + return map { + when (it) { + is WalletManageButton.Buy -> { + it.copy( + enabled = buyStatus.isContent(), + dimContent = !buyStatus.isContent(), + ) + } + is WalletManageButton.Sell -> { + it.copy( + enabled = sellStatus.isContent(), + dimContent = !sellStatus.isContent(), + ) + } + is WalletManageButton.Swap -> { + it.copy( + enabled = swapStatus.isContent(), + dimContent = !swapStatus.isContent(), + ) + } + else -> it + } + } + .toPersistentList() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 668118d2fb..11f5063820 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -27,8 +27,8 @@ internal class TokenListStateConverter( private val tokenStatusConverter = TokenItemStateConverter( appCurrency = appCurrency, - onItemClick = clickIntents::onTokenItemClick, - onItemLongClick = clickIntents::onTokenItemLongClick, + onItemClick = { _, status -> clickIntents.onTokenItemClick(status) }, + onItemLongClick = { _, status -> clickIntents.onTokenItemLongClick(status) }, ) override fun convert(value: WalletTokensListState): WalletTokensListState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 28ce2908d0..6b9dda9803 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -9,6 +9,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow @@ -21,6 +22,7 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class WalletLoadingStateFactory( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val walletFeatureToggles: WalletFeatureToggles, ) { fun create(userWallet: UserWallet): WalletState { @@ -35,6 +37,7 @@ internal class WalletLoadingStateFactory( return WalletState.MultiCurrency.Content( pullToRefreshConfig = createPullToRefreshConfig(), walletCardState = userWallet.toLoadingWalletCardState(), + buttons = createMultiWalletActions(userWallet), warnings = persistentListOf(), bottomSheetConfig = null, tokensListState = WalletTokensListState.ContentState.Loading, @@ -62,6 +65,7 @@ internal class WalletLoadingStateFactory( return WalletState.Visa.Content( pullToRefreshConfig = createPullToRefreshConfig(), walletCardState = userWallet.toLoadingWalletCardState(), + buttons = createMultiWalletActions(userWallet), warnings = persistentListOf(), bottomSheetConfig = null, balancesAndLimitBlockState = BalancesAndLimitsBlockState.Loading, @@ -89,6 +93,28 @@ internal class WalletLoadingStateFactory( ) } + private fun createMultiWalletActions(userWallet: UserWallet): PersistentList { + if (!walletFeatureToggles.isMainActionButtonsEnabled) return persistentListOf() + + return persistentListOf( + WalletManageButton.Buy( + enabled = false, + dimContent = true, + onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId) }, + ), + WalletManageButton.Swap( + enabled = false, + dimContent = true, + onClick = { clickIntents.onMultiWalletSwapClick(userWalletId = userWallet.walletId) }, + ), + WalletManageButton.Sell( + enabled = false, + dimContent = true, + onClick = { clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId) }, + ), + ) + } + private fun createDimmedButtons(): PersistentList { return persistentListOf( WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionsSubscriber.kt new file mode 100644 index 0000000000..266bb7b67c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionsSubscriber.kt @@ -0,0 +1,52 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.transformers.UpdateMultiWalletActionsTransformer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach + +/** + * Multi-currency wallet subscriber for actions state updating + * + * @property userWallet user wallet + * @property rampStateManager ramp state manager + * @property stateController state controller + * +[REDACTED_AUTHOR] + */ +internal class MultiWalletActionsSubscriber( + private val userWallet: UserWallet, + private val rampStateManager: RampStateManager, + private val stateController: WalletStateController, +) : WalletSubscriber() { + + override fun create(coroutineScope: CoroutineScope): Flow<*> { + return combine( + flow = rampStateManager.getBuyInitializationStatus(), + flow2 = rampStateManager.getSellInitializationStatus(), + flow3 = rampStateManager.getSwapInitializationStatus(userWalletId = userWallet.walletId), + transform = ::RampStatuses, + ) + .onEach { statuses -> + stateController.update( + UpdateMultiWalletActionsTransformer( + userWalletId = userWallet.walletId, + buyStatus = statuses.buy, + sellStatus = statuses.sell, + swapStatus = statuses.swap, + ), + ) + } + } + + private data class RampStatuses( + val buy: Lce, + val sell: Lce, + val swap: Lce, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index c1d3d9cd26..2617c1d2e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -75,8 +75,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.PushNotificat import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.actions import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton -import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.controlButtons import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.BalancesAndLimitsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDetailsBottomSheet @@ -191,12 +191,23 @@ private fun WalletContent( ) } - (selectedWallet as? WalletState.SingleCurrency)?.let { - controlButtons( - configs = it.buttons, - selectedWalletIndex = selectedWalletIndex, - modifier = movableItemModifier.padding(top = betweenItemsPadding), - ) + when (selectedWallet) { + is WalletState.MultiCurrency, + is WalletState.Visa, + -> { + actions( + actions = selectedWallet.buttons, + selectedWalletIndex = selectedWalletIndex, + modifier = movableItemModifier.padding(top = betweenItemsPadding), + ) + } + is WalletState.SingleCurrency -> { + lazyActions( + actions = selectedWallet.buttons, + selectedWalletIndex = selectedWalletIndex, + modifier = movableItemModifier.padding(top = betweenItemsPadding), + ) + } } notifications(configs = selectedWallet.warnings, modifier = itemModifier) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletActionButtons.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletActionButtons.kt new file mode 100644 index 0000000000..31a46d0a6a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletActionButtons.kt @@ -0,0 +1,74 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.common + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.buttons.HorizontalActionChips +import com.tangem.core.ui.components.buttons.actions.ActionButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +private const val ACTIONS_CONTENT_TYPE = "Actions" + +/** + * Actions in scrollable container. Like, "Buy", "Sell", etc + * + * @param actions list of buttons + * @param selectedWalletIndex selected wallet index for creating a unique key + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +internal fun LazyListScope.lazyActions( + actions: ImmutableList, + selectedWalletIndex: Int, + modifier: Modifier = Modifier, +) { + item(key = ACTIONS_CONTENT_TYPE + selectedWalletIndex, contentType = ACTIONS_CONTENT_TYPE) { + HorizontalActionChips( + buttons = actions.map(WalletManageButton::config).toImmutableList(), + modifier = modifier.animateItem(), + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), + ) + } +} + +/** + * Action buttons. Like, "Buy", "Sell", etc + * + * @param actions list of buttons + * @param selectedWalletIndex selected wallet index for creating a unique key + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +internal fun LazyListScope.actions( + actions: ImmutableList, + selectedWalletIndex: Int, + modifier: Modifier = Modifier, +) { + if (actions.isEmpty()) return + + item(key = ACTIONS_CONTENT_TYPE + selectedWalletIndex, contentType = ACTIONS_CONTENT_TYPE) { + Row( + modifier = modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .animateItem(), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + actions.fastForEach { + key(it::class.java) { + ActionButton(config = it.config, modifier = Modifier.weight(1f)) + } + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyControlButtons.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyControlButtons.kt deleted file mode 100644 index 4b73b51b81..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyControlButtons.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.buttons.HorizontalActionChips -import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList - -private const val CONTROL_BUTTONS_CONTENT_TYPE = "ControlButtons" - -/** - * Single currency control buttons. Like, "Buy", "Sell", etc - * - * @param configs list of buttons - * @param selectedWalletIndex selected wallet index for creating a unique key - * @param modifier modifier - * -[REDACTED_AUTHOR] - */ -@OptIn(ExperimentalFoundationApi::class) -internal fun LazyListScope.controlButtons( - configs: ImmutableList, - selectedWalletIndex: Int, - modifier: Modifier = Modifier, -) { - item(key = CONTROL_BUTTONS_CONTENT_TYPE + selectedWalletIndex, contentType = CONTROL_BUTTONS_CONTENT_TYPE) { - HorizontalActionChips( - buttons = configs.map(WalletManageButton::config).toImmutableList(), - modifier = modifier.animateItemPlacement(), - contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index fc787ace14..a9af33b972 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -31,6 +31,7 @@ import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvid import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -69,6 +70,7 @@ internal class WalletViewModel @Inject constructor( private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, private val walletImageResolver: WalletImageResolver, private val tokenListStore: MultiWalletTokenListStore, + private val walletFeatureToggles: WalletFeatureToggles, analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { @@ -300,6 +302,7 @@ internal class WalletViewModel @Inject constructor( wallets = action.wallets, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + walletFeatureToggles = walletFeatureToggles, ), ) @@ -334,6 +337,7 @@ internal class WalletViewModel @Inject constructor( newUserWallet = action.selectedWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + walletFeatureToggles = walletFeatureToggles, ), ) } @@ -350,6 +354,7 @@ internal class WalletViewModel @Inject constructor( userWallet = action.selectedWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + walletFeatureToggles = walletFeatureToggles, ), ) @@ -401,6 +406,7 @@ internal class WalletViewModel @Inject constructor( unlockedWallets = action.unlockedWallets, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + walletFeatureToggles = walletFeatureToggles, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index 0c716472a9..f40ea9b059 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase @@ -20,6 +21,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetToken import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import javax.inject.Inject @@ -41,6 +44,7 @@ internal class WalletClickIntents @Inject constructor( private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview, + private val rampStateManager: RampStateManager, private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), WalletCardClickIntents by walletCardClickIntentsImplementor, @@ -116,6 +120,12 @@ internal class WalletClickIntents @Inject constructor( fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL) } + listOf( + async { rampStateManager.fetchBuyServiceData() }, + async { rampStateManager.fetchSellServiceData() }, + ) + .awaitAll() + maybeFetchResult.onLeft { stateHolder.update( SetTokenListErrorTransformer( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 21fcf81bf1..23d45eab8b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -81,6 +81,12 @@ interface WalletCurrencyActionsClickIntents { fun onExploreClick() fun onAnalyticsClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onMultiWalletBuyClick(userWalletId: UserWalletId) + + fun onMultiWalletSellClick(userWalletId: UserWalletId) + + fun onMultiWalletSwapClick(userWalletId: UserWalletId) } @Suppress("LongParameterList", "LargeClass") @@ -374,7 +380,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( appRouter.push( AppRoute.Swap( - currency = cryptoCurrencyStatus.currency, + currencyFrom = cryptoCurrencyStatus.currency, userWalletId = userWalletId, ), ) @@ -432,6 +438,21 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } } + override fun onMultiWalletSellClick(userWalletId: UserWalletId) { + // TODO [REDACTED_JIRA] + appRouter.push(route = AppRoute.SellCrypto(userWalletId = userWalletId)) + } + + override fun onMultiWalletSwapClick(userWalletId: UserWalletId) { + // TODO [REDACTED_JIRA] + appRouter.push(route = AppRoute.SwapCrypto(userWalletId = userWalletId)) + } + + override fun onMultiWalletBuyClick(userWalletId: UserWalletId) { + // TODO [REDACTED_JIRA] + appRouter.push(route = AppRoute.BuyCrypto(userWalletId = userWalletId)) + } + private fun openExplorer() { val userWalletId = stateHolder.getSelectedWalletId() diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 31944d51c4..2e3c74646e 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -87,9 +87,9 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.18-862" +tangemBlockchainSdk = "develop-863" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.18-398" +tangemCardSdk = "develop-400" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem17" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 52bc74e7f3..46c633a3c9 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -138,6 +138,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "core/test" -> Blockchain.CoreTestnet "casper-network" -> Blockchain.Casper "casper-network/test" -> Blockchain.CasperTestnet + "chiliz" -> Blockchain.Chiliz + "chiliz/test" -> Blockchain.ChilizTestnet "xodex" -> Blockchain.Xodex "canxium" -> Blockchain.Canxium else -> null @@ -277,6 +279,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.CasperTestnet -> "casper-network/test" Blockchain.Core -> "core" Blockchain.CoreTestnet -> "core/test" + Blockchain.Chiliz -> "chiliz" + Blockchain.ChilizTestnet -> "chiliz/test" Blockchain.Xodex -> "xodex" Blockchain.Canxium -> "canxium" } @@ -368,6 +372,7 @@ fun Blockchain.toCoinId(): String { Blockchain.EnergyWebX, Blockchain.EnergyWebXTestnet -> "energy-web-token" Blockchain.Casper, Blockchain.CasperTestnet -> "casper-network" Blockchain.Core, Blockchain.CoreTestnet -> "coredaoorg" + Blockchain.Chiliz, Blockchain.ChilizTestnet -> "chiliz" Blockchain.Xodex -> "xodex" Blockchain.Canxium -> "canxium" } diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/BackupServiceHolder.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/BackupServiceHolder.kt new file mode 100644 index 0000000000..85795081fc --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/BackupServiceHolder.kt @@ -0,0 +1,17 @@ +package com.tangem.sdk.api + +import androidx.activity.ComponentActivity +import com.tangem.TangemSdk +import com.tangem.operations.backup.BackupService +import com.tangem.sdk.extensions.init +import java.lang.ref.WeakReference + +class BackupServiceHolder { + + lateinit var backupService: WeakReference + private set + + fun createAndSetService(tangemSdk: TangemSdk, activity: ComponentActivity) { + backupService = WeakReference(BackupService.init(tangemSdk, activity)) + } +} \ No newline at end of file diff --git a/version.properties b/version.properties index 3a969ad8fe..3e279d9f6c 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -versionName=5.18.0 \ No newline at end of file +versionName=5.19.0 \ No newline at end of file