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/tangem-app-config b/app/src/main/assets/tangem-app-config index adbdabe422..cce49feacb 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit adbdabe422b0513640d1750f276298683d342f61 +Subproject commit cce49feacb18dc9ad7cc5da62d58f1b3338a1057 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/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 8bc1f8e7ae..c48a2988f8 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -140,6 +140,11 @@ class DialogManager : StoreSubscriber { message = state.dialog.error.message, context = context, ) + is WalletConnectDialog.UnsupportedWcVersion -> SimpleAlertDialog.create( + titleRes = R.string.common_error, + messageRes = R.string.unsupported_wc_version, + context = context, + ) is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context) is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context) is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context) 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..d21153b84b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -0,0 +1,105 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.onramp.* +import com.tangem.domain.onramp.repositories.OnrampErrorResolver +import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +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) + } + + @Provides + @Singleton + fun provideGetOnrampStatusUseCase( + onrampRepository: OnrampRepository, + onrampErrorResolver: OnrampErrorResolver, + ): GetOnrampStatusUseCase { + return GetOnrampStatusUseCase(onrampRepository, onrampErrorResolver) + } + + @Provides + @Singleton + fun provideGetOnrampCurrencyUseCase(onrampRepository: OnrampRepository): GetOnrampCurrencyUseCase { + return GetOnrampCurrencyUseCase(onrampRepository) + } + + @Provides + @Singleton + fun provideGetOnrampTransactionsUseCase( + onrampTransactionRepository: OnrampTransactionRepository, + ): GetOnrampTransactionsUseCase { + return GetOnrampTransactionsUseCase(onrampTransactionRepository) + } + + @Provides + @Singleton + fun provideGetOnrampTransactionUseCase( + onrampTransactionRepository: OnrampTransactionRepository, + ): GetOnrampTransactionUseCase { + return GetOnrampTransactionUseCase(onrampTransactionRepository) + } + + @Provides + @Singleton + fun provideOnrampRemoveTransactionUseCase( + onrampTransactionRepository: OnrampTransactionRepository, + ): OnrampRemoveTransactionUseCase { + return OnrampRemoveTransactionUseCase(onrampTransactionRepository) + } + + @Provides + @Singleton + fun provideOnrampSaveTransactionUseCase( + onrampTransactionRepository: OnrampTransactionRepository, + ): OnrampSaveTransactionUseCase { + return OnrampSaveTransactionUseCase(onrampTransactionRepository) + } + + @Provides + @Singleton + fun provideGetOnrampPaymentMethodsUseCase(onrampRepository: OnrampRepository): GetOnrampPaymentMethodsUseCase { + return GetOnrampPaymentMethodsUseCase(onrampRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 8ec26eb230..9d42683053 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -133,14 +133,6 @@ internal object SettingsDomainModule { return ShouldShowRingPromoUseCase(promoSettingsRepository) } - @Provides - @Singleton - fun provideShouldShowTravalaPromoWalletUseCase( - promoSettingsRepository: PromoSettingsRepository, - ): ShouldShowTravalaPromoWalletUseCase { - return ShouldShowTravalaPromoWalletUseCase(promoSettingsRepository) - } - @Provides @Singleton fun provideShouldShowSwapPromoTokenUseCase( 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/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index 06591c01b0..476bcd288d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -37,5 +37,7 @@ sealed class WalletConnectAction : Action { data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction() data class PairConnectErrorAction(val throwable: Throwable) : WalletConnectAction() + + data object UnsupportedDappRequest : WalletConnectAction() //endregion WalletConnect 2.0 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index facfd60eab..fd477038f6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -78,6 +78,12 @@ class WalletConnectMiddleware { val index = action.wcUri.indexOf("@") when (action.wcUri[index + 1]) { '2' -> walletConnectRepository.pair(uri = action.wcUri) + '1' -> { + store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest) + store.dispatchOnMain( + GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedWcVersion), + ) + } } } is WalletConnectAction.RejectRequest -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt index a7640ed758..1cf6c4da24 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt @@ -15,6 +15,7 @@ object WalletConnectReducer { is WalletConnectAction.SessionEstablished, is WalletConnectAction.SessionRejected, is WalletConnectAction.PairConnectErrorAction, + is WalletConnectAction.UnsupportedDappRequest, -> state.copy(loading = false) is WalletConnectAction.SessionListUpdated -> state.copy( wc2Sessions = action.sessions, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index d1a9368288..d557aa6910 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -82,6 +82,7 @@ data class WalletForSession( } sealed class WalletConnectDialog : StateDialog { + data object UnsupportedWcVersion : WalletConnectDialog() data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog() data object UnsupportedCard : WalletConnectDialog() data class UnsupportedNetwork(val networks: List? = null) : WalletConnectDialog() 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 58d13a9b0e..1c1ea6340e 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 @@ -197,11 +197,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) @@ -209,6 +209,7 @@ private fun handleFinishBackup(scanResponse: ScanResponse) { } OnboardingHelper.saveWallet( + alreadyCreatedWallet = userWallet, scanResponse = updatedScanResponse, accessCode = backupState.accessCode, backupCardsIds = backupState.backupCardIds, @@ -679,7 +680,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)) } @@ -740,6 +741,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..46a9c70161 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,8 @@ 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.OnrampComponent -import com.tangem.features.onramp.component.SellCryptoComponent +import com.tangem.features.onramp.component.* import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter @@ -51,8 +49,10 @@ internal class ChildFactory @Inject constructor( private val manageTokensComponentFactory: ManageTokensComponent.Factory, private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, private val onrampComponentFactory: OnrampComponent.Factory, + private val onrampSuccessComponentFactory: OnrampSuccessComponent.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,10 +197,17 @@ internal class ChildFactory @Inject constructor( is AppRoute.Onramp -> { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = OnrampComponent.Params(), + params = OnrampComponent.Params(route.currency), componentFactory = onrampComponentFactory, ) } + is AppRoute.OnrampSuccess -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = OnrampSuccessComponent.Params(route.txId), + componentFactory = onrampSuccessComponentFactory, + ) + } is AppRoute.BuyCrypto -> { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), @@ -215,10 +222,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..da4f005d32 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,16 @@ 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 OnrampSuccess( + val txId: String, + ) : AppRoute(path = "/onramp/success/$txId"), RouteBundleParams { + override fun getBundle(): Bundle = bundle(serializer()) + } @Serializable data class BuyCrypto( @@ -299,6 +316,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/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 5d8b37fd46..4f4e74d559 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -31,10 +31,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowDefault -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.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList @@ -118,7 +115,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh @Composable private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) { FooterContainer( - footer = stringResource(id = R.string.give_permission_policy_type_footer), + footer = resourceReference(R.string.give_permission_policy_type_footer), modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { AmountItem( @@ -130,7 +127,7 @@ private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) } SpacerH16() FooterContainer( - footer = data.footerText.resolveReference(), + footer = data.footerText, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { FeeItem(fee = data.fee) diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt new file mode 100644 index 0000000000..cdf3f1f8fb --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBlock.kt @@ -0,0 +1,228 @@ +package com.tangem.common.ui.expressStatus + +import androidx.annotation.DrawableRes +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.R +import com.tangem.common.ui.expressStatus.state.ExpressLinkUM +import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState +import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM +import com.tangem.common.ui.expressStatus.state.ExpressStatusUM +import com.tangem.core.ui.components.SpacerWMax +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 kotlinx.collections.immutable.persistentListOf + +/** + * Block with express statuses + * + * @param state ui holder + * @param modifier modifier + * @see [Figma](https://www.figma.com/design/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=18459-26521&t=4jox7bfqUiXnm2h1-4) + */ +@Composable +fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding( + vertical = TangemTheme.dimens.spacing14, + horizontal = TangemTheme.dimens.spacing12, + ), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing16), + ) { + Text( + text = state.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + SpacerWMax() + AnimatedVisibility(visible = state.link is ExpressLinkUM.Content) { + val link = remember(this) { state.link as ExpressLinkUM.Content } + Row( + modifier = Modifier.clickable { link.onClick() }, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = link.icon), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(TangemTheme.dimens.spacing16) + .padding(end = TangemTheme.dimens.spacing2), + ) + Text( + text = link.text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } + + Column { + state.statuses.forEachIndexed { index, item -> + ExpressStatusStep(item, index == state.statuses.lastIndex) + } + } + } +} + +@Composable +private fun ExpressStatusStep(status: ExpressStatusItemUM, isLast: Boolean) { + AnimatedContent( + targetState = status, + label = "Exchange Step Change Success", + transitionSpec = { + fadeIn(tween(durationMillis = 220)) togetherWith + fadeOut(tween(durationMillis = 220)) + }, + ) { content -> + Row { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (content.state) { + ExpressStatusItemState.Active -> StepInProgress() + ExpressStatusItemState.Default -> StepDefault() + ExpressStatusItemState.Done -> Step( + iconRes = R.drawable.ic_check_24, + iconColor = TangemTheme.colors.icon.primary1, + borderColor = TangemTheme.colors.field.focused, + ) + ExpressStatusItemState.Error -> Step( + iconRes = R.drawable.ic_close_24, + iconColor = TangemTheme.colors.icon.warning, + ) + ExpressStatusItemState.Warning -> Step( + iconRes = R.drawable.ic_close_24, + iconColor = TangemTheme.colors.icon.attention, + ) + } + if (!isLast) { + StepSeparator() + } + } + val textColor = when (status.state) { + ExpressStatusItemState.Active -> TangemTheme.colors.text.primary1 + ExpressStatusItemState.Default -> TangemTheme.colors.text.disabled + ExpressStatusItemState.Done -> TangemTheme.colors.text.primary1 + ExpressStatusItemState.Error -> TangemTheme.colors.text.warning + ExpressStatusItemState.Warning -> TangemTheme.colors.text.attention + } + Text( + text = content.text.resolveReference(), + style = TangemTheme.typography.body2, + color = textColor, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12), + ) + } + } +} + +@Composable +private fun StepDefault() { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size20) + .border( + width = TangemTheme.dimens.size1_5, + color = TangemTheme.colors.field.focused, + shape = CircleShape, + ) + .padding(TangemTheme.dimens.spacing2), + ) +} + +@Composable +private fun Step(iconColor: Color, @DrawableRes iconRes: Int, borderColor: Color = iconColor) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = iconColor, + modifier = Modifier + .size(TangemTheme.dimens.size20) + .border( + width = TangemTheme.dimens.size1_5, + color = borderColor, + shape = CircleShape, + ) + .padding(TangemTheme.dimens.spacing2), + ) +} + +@Composable +private fun StepInProgress() { + CircularProgressIndicator( + color = TangemTheme.colors.icon.primary1, + strokeWidth = TangemTheme.dimens.size2, + modifier = Modifier + .padding(TangemTheme.dimens.spacing2) + .size(TangemTheme.dimens.size14), + ) +} + +@Composable +private fun StepSeparator() { + Box( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing2) + .size( + width = TangemTheme.dimens.size1_5, + height = TangemTheme.dimens.size10, + ) + .background( + color = TangemTheme.colors.field.focused, + shape = CircleShape, + ), + ) +} + +@Preview +@Composable +private fun Preview_ExchangeStatusBlock() { + val state = ExpressStatusUM( + title = resourceReference(R.string.express_exchange_status_title), + link = ExpressLinkUM.Content( + icon = R.drawable.ic_alert_24, + text = resourceReference(R.string.common_go_to_provider), + onClick = {}, + ), + statuses = persistentListOf( + ExpressStatusItemUM(text = stringReference("Done"), state = ExpressStatusItemState.Done), + ExpressStatusItemUM(text = stringReference("Active"), state = ExpressStatusItemState.Active), + ExpressStatusItemUM(text = stringReference("Warning"), state = ExpressStatusItemState.Warning), + ExpressStatusItemUM(text = stringReference("Error"), state = ExpressStatusItemState.Error), + ExpressStatusItemUM(text = stringReference("Default"), state = ExpressStatusItemState.Default), + ), + ) + + TangemThemePreview { + ExpressStatusBlock(state = state) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusNotificationBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusNotificationBlock.kt new file mode 100644 index 0000000000..b93ad58334 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusNotificationBlock.kt @@ -0,0 +1,32 @@ +package com.tangem.common.ui.expressStatus + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.notifications.ExpressNotificationsUM +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun ExpressStatusNotificationBlock(state: NotificationUM?) { + AnimatedContent( + targetState = state, + modifier = Modifier.padding(top = 12.dp), + label = "Express Status Notification Change", + ) { notification -> + if (notification?.config != null) { + Notification( + config = notification.config, + iconTint = when (state) { + is ExpressNotificationsUM.NeedVerification -> TangemTheme.colors.icon.attention + is ExpressNotificationsUM.FailedByProvider -> TangemTheme.colors.icon.warning + else -> null + }, + containerColor = TangemTheme.colors.background.action, + ) + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressStatusUM.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressStatusUM.kt new file mode 100644 index 0000000000..6fcb02cc30 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressStatusUM.kt @@ -0,0 +1,54 @@ +package com.tangem.common.ui.expressStatus.state + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Stable +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +/** + * UI data holder for express status block + * + * @property title block title + * @property link provider web link + * @property statuses list of possible and active statuses + */ +data class ExpressStatusUM( + val title: TextReference, + val link: ExpressLinkUM, + val statuses: ImmutableList, +) + +/** + * Provider web link for express status block. + * [Empty] if no link needed + * [Content] if link is provided and displayed + */ +@Stable +sealed class ExpressLinkUM { + data object Empty : ExpressLinkUM() + data class Content( + @DrawableRes val icon: Int, + val text: TextReference, + val onClick: () -> Unit, + ) : ExpressLinkUM() +} + +/** + * Single status item in express status block + */ +data class ExpressStatusItemUM( + val text: TextReference, + val state: ExpressStatusItemState, +) + +/** + * Available status states for express status block + */ +enum class ExpressStatusItemState { + Active, + Default, + Done, + Warning, + Error, + ; +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/ExpressNotificationsUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/ExpressNotificationsUM.kt new file mode 100644 index 0000000000..09c66ed8c8 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/ExpressNotificationsUM.kt @@ -0,0 +1,28 @@ +package com.tangem.common.ui.notifications + +import com.tangem.common.ui.R +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resourceReference + +object ExpressNotificationsUM { + + data class NeedVerification(val onGoToProviderClick: () -> Unit) : NotificationUM.Warning( + title = resourceReference(R.string.express_exchange_notification_verification_title), + subtitle = resourceReference(R.string.express_exchange_notification_verification_text), + iconResId = R.drawable.ic_alert_triangle_20, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_go_to_provider), + onClick = onGoToProviderClick, + ), + ) + + data class FailedByProvider(val onGoToProviderClick: () -> Unit) : NotificationUM.Error( + title = resourceReference(R.string.express_exchange_notification_failed_title), + subtitle = resourceReference(R.string.express_exchange_notification_failed_text), + iconResId = R.drawable.ic_alert_circle_24, + buttonState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_go_to_provider), + onClick = onGoToProviderClick, + ), + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index 858fdccb46..83636c2a98 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.shorted +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import java.math.BigDecimal sealed class NotificationUM(val config: NotificationConfig) { @@ -307,4 +308,17 @@ sealed class NotificationUM(val config: NotificationConfig) { ), ) } + + sealed interface Solana { + + data class RentInfo( + private val rentInfo: CryptoCurrencyWarning.Rent, + ) : Info( + title = TextReference.Res(R.string.warning_rent_fee_title), + subtitle = TextReference.Res( + id = R.string.warning_solana_rent_fee_message, + formatArgs = wrappedList(rentInfo.rent, rentInfo.exemptionAmount), + ), + ) + } } \ 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/common/adapter/DateTimeAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt index 5e637ed8c8..4149bf22f8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt @@ -7,9 +7,12 @@ internal class DateTimeAdapter : JsonAdapter() { @FromJson override fun fromJson(reader: JsonReader): DateTime? { - val dateString = reader.nextString() ?: return null - - return DateTime.parse(dateString) + return if (reader.peek() == JsonReader.Token.NULL) { + reader.nextNull() + } else { + val dateString = reader.nextString() + DateTime.parse(dateString) + } } @ToJson diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt index 6a7cd420c6..d87504268b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt @@ -10,8 +10,12 @@ internal class LocalDateAdapter : JsonAdapter() { @FromJson override fun fromJson(reader: JsonReader): LocalDate? { - val dateString = reader.nextString() - return LocalDate.parse(dateString, formatter) + return if (reader.peek() == JsonReader.Token.NULL) { + reader.nextNull() + } else { + val dateString = reader.nextString() + return LocalDate.parse(dateString, formatter) + } } @ToJson diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index 64b168072f..e18e222fd9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.markets.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.joda.time.DateTime import java.math.BigDecimal @JsonClass(generateAdapter = true) @@ -26,6 +27,8 @@ data class TokenMarketInfoResponse( val insights: Insights?, @Json(name = "metrics") val metrics: Metrics?, + @Json(name = "security_data") + val securityData: SecurityData?, @Json(name = "links") val links: Links?, @Json(name = "price_performance") @@ -144,6 +147,28 @@ data class TokenMarketInfoResponse( val allTime: Range?, ) + @JsonClass(generateAdapter = true) + data class SecurityData( + @Json(name = "total_security_score") + val totalSecurityScore: Float, + @Json(name = "provider_data") + val providerData: List, + ) + + @JsonClass(generateAdapter = true) + data class ProviderData( + @Json(name = "provider_id") + val providerId: String, + @Json(name = "provider_name") + val providerName: String, + @Json(name = "link") + val link: String?, + @Json(name = "security_score") + val securityScore: Float, + @Json(name = "last_audit_date") + val lastAuditDate: DateTime?, + ) + @JsonClass(generateAdapter = true) data class Range( @Json(name = "low_price") 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/api/onramp/models/response/OnrampStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampStatusResponse.kt index 6ef0416464..301fd2421f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampStatusResponse.kt @@ -14,8 +14,8 @@ data class OnrampStatusResponse( @Json(name = "payoutAddress") val payoutAddress: String, - // @Json(name = "status") - // val status: ??? + @Json(name = "status") + val status: Status, @Json(name = "failReason") val failReason: String?, @@ -48,14 +48,46 @@ data class OnrampStatusResponse( val toDecimals: String, @Json(name = "toAmount") - val toAmount: String, + val toAmount: String?, @Json(name = "toActualAmount") - val toActualAmount: String, + val toActualAmount: String?, @Json(name = "paymentMethod") val paymentMethod: String, @Json(name = "countryCode") val countryCode: String, -) \ No newline at end of file +) + +enum class Status { + @Json(name = "created") + Created, + + @Json(name = "expired") + Expired, + + @Json(name = "waiting-for-payment") + WaitingForPayment, + + @Json(name = "payment-processing") + PaymentProcessing, + + @Json(name = "verifying") + Verifying, + + @Json(name = "failed") + Failed, + + @Json(name = "paid") + Paid, + + @Json(name = "sending") + Sending, + + @Json(name = "finished") + Finished, + + @Json(name = "paused") + Paused, +} \ 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/OnrampStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/OnrampStoreModule.kt new file mode 100644 index 0000000000..8b7fb87fa2 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/OnrampStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.onramp.DefaultOnrampPaymentMethodsStore +import com.tangem.datasource.local.onramp.OnrampPaymentMethodsStore +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 OnrampStoreModule { + + @Provides + @Singleton + fun provideOnrampPaymentMethodsStore(): OnrampPaymentMethodsStore { + return DefaultOnrampPaymentMethodsStore(dataStore = RuntimeDataStore()) + } +} \ No newline at end of file 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/onramp/DefaultOnrampPaymentMethodsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/onramp/DefaultOnrampPaymentMethodsStore.kt new file mode 100644 index 0000000000..3a4fa1b949 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/onramp/DefaultOnrampPaymentMethodsStore.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.local.onramp + +import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator + +internal class DefaultOnrampPaymentMethodsStore( + dataStore: StringKeyDataStore>, +) : OnrampPaymentMethodsStore, StringKeyDataStoreDecorator>(dataStore) { + + override fun provideStringKey(key: String): String = key +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/onramp/OnrampPaymentMethodsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/onramp/OnrampPaymentMethodsStore.kt new file mode 100644 index 0000000000..a6847ae671 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/onramp/OnrampPaymentMethodsStore.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.local.onramp + +import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO + +interface OnrampPaymentMethodsStore { + + suspend fun getSyncOrNull(key: String): List? + + suspend fun store(key: String, value: List) + + suspend fun contains(key: String): Boolean +} \ 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..5fd333db8a 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 @@ -53,10 +53,6 @@ object PreferencesKeys { val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") } - val IS_WALLET_TRAVALA_PROMO_SHOWN_KEY by lazy { - booleanPreferencesKey(name = "isWalletTravalaPromoShown") - } - val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") } val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") } @@ -103,6 +99,12 @@ 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") } + + val ONRAMP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "onrampTransactionsStatuses") } + // region Permission fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index a7fd3cafef..0369cf22cd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -150,4 +150,12 @@ suspend inline fun AppPreferencesStore.getObjectSetSync(key: Prefere ?.get(key) ?.let(adapter::fromJson) .orEmpty() +} + +/** Get flow of set of [T] by string [key], or empty if data is not found */ +inline fun AppPreferencesStore.getObjectSet(key: Preferences.Key): Flow> { + val adapter = moshi.adapter>(Types.newParameterizedType(Set::class.java, T::class.java)) + return data.map { + it[key]?.let(adapter::fromJson) ?: emptySet() + } } \ No newline at end of file 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 d8c34ccdc5..d5b6d3041c 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -908,6 +908,7 @@ Diese Aktion ist unumkehrbar. Du hast keinen Zugriff mehr auf die alte Wallet. Tippe auf die Doppelkarte oder Ring mit der Nummer %s und entferne sie erst am Ende des Vorgangs. Verwende %s oder scanne eine Karte oder Ring, um Zugriff auf deine Wallet zu erhalten. + Verbindung fehlgeschlagen: Diese dApp verwendet Wallet Connect Version 1.0, die nicht unterstützt wird. Bitte stelle sicher, dass die dApp Wallet Connect Version 2.0 unterstützt, um eine erfolgreiche Verbindung herzustellen. Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten Sei der Erste, der von neuen Aktionen erfährt Möchtest du Push-Benachrichtigungen verwenden? diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index b77f4a6456..c65acf41fa 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -294,6 +294,9 @@ No disponible para este par Permiso requerido Recomendado + Comprado %s + Comprando %s + Comprando %s... No se encontraron fichas. Por favor intenta con otra solicitud ID : %s ID de transacción copiado @@ -907,6 +910,7 @@ Esta acción es irreversible. No tendrá acceso la billetera antigua. Toque la tarjeta gemela con el número %s y no la retira hasta el final de la operación Use %s o escanee una tarjeta/anillo para tener acceso a su billetera + Error de conexión: Esta dApp utiliza la versión 1.0 de Wallet Connect, que no es compatible. Asegúrese de que la dApp sea compatible con la versión 2.0 de Wallet Connect para conectarse correctamente. Manténgase actualizado con las últimas funciones y noticias Sea el primero en enterarte de nuevas promociones ¿Quiere utilizar\nnotificaciones push? @@ -1006,6 +1010,9 @@ Este token debe estar asociado con su cuenta de Hedera antes de poder recibirlo. Asocie su token No hay suficiente %s. Recargue su cuenta de Hedera para asociar este token + ¿Está seguro(a) de que desea cancelar la transacción? No podrá volver a intentar realizarla. + Su transacción con un importe de %1$s %2$s no se ha completado. Puede volver a intentarlo para completarla. + Tiene una transacción sin terminar Solo quedan %s firmas en esta tarjeta. Deba retirar todos sus fondos. Recuento de firmas bajo Los tokens en diferentes redes pueden tener direcciones diferentes. Verifique que su dirección coincida con la red cuando transfieras fondos. diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 346d3081e5..1d37bbc265 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -180,6 +180,7 @@ Conditions d\'utilisation Aujourd\'hui La transaction a échoué + Statut de la transaction Transactions Transfert Je comprends @@ -187,6 +188,7 @@ Inaccessible Unstakez semaine + avec Oui Adresse du contrat copiée ! Réseaux disponibles @@ -292,6 +294,9 @@ Indisponible pour cette paire Permission requise Recommandé + Acheté %s + Achat en cours %s + Achat en cours %s... Aucun jeton trouvé. Veuillez essayer une autre demande ID : %s ID de transaction copié @@ -427,6 +432,7 @@ La valeur théorique totale d\'une crypto-monnaie si toutes les pièces qui pourraient exister étaient en circulation, y compris celles qui ne circulent pas actuellement Valorisation entièrement diluée Date de la Genesis + Vide Haut Détenteurs L\'évolution du nombre de détenteurs de jetons au cours d\'une période donnée @@ -437,6 +443,7 @@ Le changement dans la quantité de liquidité disponible pour le jeton pendant la période spécifiée Liquidité Indice de liquidité + Vide Listé sur Faible Cap. boursière @@ -453,6 +460,7 @@ Performance des prix Dépôt Score de sécurité + Le score de sécurité d\'un jeton est une mesure qui évalue le niveau de sécurité d\'une blockchain ou d\'un jeton en fonction de divers facteurs. Ceci est compilé à partir des sources répertoriées ci-dessous. Social Approvisionnement total Le nombre maximal de pièces ou de jetons pouvant exister pour une crypto-monnaie particulière @@ -509,12 +517,12 @@ Sauvegarde en cours En savoir plus sur les seed phrases - + Vide Écrivez ces %d mots dans l\'ordre indiqué ci-dessous et conservez-les dans un endroit sûr et secret. Votre seed phrase - + Vide %d mots Pour importer votre portefeuille, entrez votre seed phrase dans le champ ci-dessous @@ -566,12 +574,15 @@ Le montant à acheter doit être au moins %s Aucun fournisseur disponible pour cette devise Payer avec + Vous pourrez finaliser votre transaction sur le fournisseur tiers, %s + Redirection vers %s... Nos services ne sont pas disponibles dans ce pays Modifiez-le ou confirmez-le Votre résidence a été identifiée comme Résidence Veuillez sélectionner le bon pays pour garantir des options de paiement et des services précis. Paramètres + Vous pouvez vérifier l\'état de la transaction à partir de la page du jeton Via Grouper Par solde @@ -899,6 +910,7 @@ Cette action est irréversible. Vous n\'aurez plus accès à l\'ancien portefeuille. Appuyez sur la carte jumelle avec le numéro %s et ne la retirez pas jusqu\'à la fin de l\'opération Utilisez %s ou scannez une carte/bague pour avoir accès à votre portefeuille + Échec de la connexion : Cette dApp utilise Wallet Connect version1.0, qui n\'est pas prise en charge. Veuillez vous assurer que la dApp prend en charge Wallet Connect version2.0 pour réussir la connexion. Restez à jour avec les dernières fonctionnalités et actualités Soyez le premier informé des nouvelles promotions Souhaitez-vous utiliser les\nnotifications push? @@ -950,6 +962,9 @@ %s réseau L\'adresse a été copiée avec succès Pas de connexion internet + Obtenez-le maintenant avec 10 %% de réduction + Accédez à plus de 13 000 cryptomonnaies. Achetez, vendez, échangez et stakez en un seul clic.\nAssociez jusqu\'à trois cartes pour une sauvegarde. + Découvrez le Portefeuille Tangem Paramètres du portefeuille Tangem Utilisez %s ou scannez une carte/bague pour déverrouiller l\'accès à votre portefeuille @@ -995,6 +1010,9 @@ Ce jeton doit être associé à votre compte Hedera avant que vous puissiez le recevoir Associez votre jeton %s insuffisant. Renflouez votre compte Hedera pour associer ce jeton + Êtes-vous sûr(e) de vouloir annuler la transaction ? Vous ne pourrez plus réessayer. + Votre transaction d\'un montant de %1$s %2$s n\'a pas été finalisée. Vous pouvez réessayer plus tard pour la finaliser. + Vous avez une transaction inachevée Il ne reste que %s signatures sur cette carte. Vous devez retirer tous vos fonds. Faible nombre de signatures Les jetons sur différents réseaux peuvent avoir des adresses différentes. Vérifiez bien que votre adresse correspond au réseau lorsque vous transférez des fonds. diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 0c5037ec1c..9f3c536b40 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -898,6 +898,7 @@ この操作は元に戻せません。古いウォレットにはアクセスできなくなります。 番号%sのツインカードをタップし、操作が終了するまで取り外さないでください。 %sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください + 接続に失敗しました:このdAppは、サポートされていないWallet Connectバージョン1.0を使用しています。正常に接続するには、dAppがWallet Connectバージョン2.0をサポートしていることを確認してください。 最新の機能とニュースをお届けします 新しいプロモーション情報をいち早く入手しましょう プッシュ通知を使用しますか? diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 280a951d4b..94815eb793 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -913,6 +913,7 @@ Это действие необратимо. У вас не будет доступа к старому кошельку. Приложите twin-карту с номером %s и не убирайте до окончания операции Используйте %s или отсканируйте карту/кольцо, чтобы получить доступ к своему кошельку + Соединение не удалось: это dApp использует Wallet Connect версии 1.0, которая не поддерживается. Убедитесь, что dApp поддерживает Wallet Connect версии 2.0 для успешного подключения. Будьте в курсе новых функций и новостей Узнавайте первым о новых акциях Хотите использовать Push-уведомления? 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 c446190dcc..b425f91e95 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -927,6 +927,7 @@ Ця дія є незворотною. Ви не матимете доступу до старого гаманця. Прикладіть twin-картку з номером %s та не прибирайте її до завершення операції Використовуйте %s або відскануйте картку/кільце, щоб отримати доступ до свого гаманця + Не вдалося встановити з\'єднання: Цей dApp використовує Wallet Connect версії 1.0, яка не підтримується. Будь ласка, переконайтеся, що dApp підтримує Wallet Connect версії 2.0 для успішного підключення. Будьте в курсі останніх функцій та новин Дізнавайтеся першими про нові акції Бажаєте використовувати Push-повідомлення? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 08a7924ae5..60cf7b6306 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -908,6 +908,7 @@ This action is irreversible. You will not have access to the old wallet. Tap the twin card with number %s and do not remove until the end of the operation Use %s or scan a card/ring to have access to your wallet + Connection failed: This dApp uses Wallet Connect version 1.0, which is not supported. Please ensure the dApp supports Wallet Connect version 2.0 to connect successfully. Stay up to date with the latest features and news Be the first to know about new promotions Would you like to use\nPush-notifications? 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 Unit, ) { Column(modifier = modifier) { content() AnimatedVisibility(visible = footer != null) { + val footerWrapped = remember(this) { requireNotNull(footer) } Text( - text = footer.orEmpty(), + text = footerWrapped.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 3d76006795..c504817b1b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -25,6 +25,7 @@ import com.tangem.core.ui.components.token.internal.* import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.rememberHapticFeedback +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -145,14 +146,16 @@ private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed is TokenItemState.Unreachable, -> { 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/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 9617ee7825..7698a8067d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -36,6 +36,7 @@ data class TangemDimens internal constructor( val radius26: Dp = 26.dp, val radius28: Dp = 28.dp, val radius36: Dp = 36.dp, + val radius40: Dp = 40.dp, // endregion Radius // region Size val size0: Dp = 0.dp, 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/core/ui/src/main/res/drawable/img_travala_banner_promo_background.webp b/core/ui/src/main/res/drawable/img_travala_banner_promo_background.webp deleted file mode 100644 index bc3845b8b7..0000000000 Binary files a/core/ui/src/main/res/drawable/img_travala_banner_promo_background.webp and /dev/null differ diff --git a/core/ui/src/main/res/drawable/img_travala_banner_promo_background_2.webp b/core/ui/src/main/res/drawable/img_travala_banner_promo_background_2.webp deleted file mode 100644 index c051b16418..0000000000 Binary files a/core/ui/src/main/res/drawable/img_travala_banner_promo_background_2.webp and /dev/null differ diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt index fb9ff22adb..047150c42e 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt @@ -14,15 +14,11 @@ import com.tangem.blockchain.common.Token as SdkToken class ResponseCryptoCurrenciesFactory { - fun createCurrency( - currencyId: CryptoCurrency.ID, - response: UserTokensResponse, - scanResponse: ScanResponse, - ): CryptoCurrency { + fun createCurrency(currencyId: String, response: UserTokensResponse, scanResponse: ScanResponse): CryptoCurrency { return response.tokens .asSequence() .mapNotNull { createCurrency(it, scanResponse) } - .first { it.id == currencyId } + .first { it.id.value == currencyId } } fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List { 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 a329974a0d..be0e41c584 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( @@ -253,9 +259,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/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt index 7df1e1a203..85afc94699 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -1,9 +1,11 @@ package com.tangem.data.markets.converters +import android.net.Uri import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse +import com.tangem.domain.markets.BuildConfig import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenQuotes import com.tangem.utils.converter.Converter @@ -11,6 +13,9 @@ import java.math.BigDecimal internal object TokenMarketInfoConverter : Converter { + private const val PROD_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/security_provider/" + private const val DEV_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api.dev/security_provider/" + override fun convert(value: TokenMarketInfoResponse): TokenMarketInfo { return with(value) { TokenMarketInfo( @@ -23,6 +28,7 @@ internal object TokenMarketInfoConverter : Converter= 2) { + parts.takeLast(2).joinToString(".") + } else { + host + } + }.getOrNull() + } + + private fun getImageHost(): String { + return if (BuildConfig.TESTER_MENU_ENABLED) { + DEV_IMAGE_HOST + } else { + PROD_IMAGE_HOST + } + } } \ No newline at end of file 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/DefaultOnrampErrorResolver.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampErrorResolver.kt index 0ec508bd89..b03b5c6c69 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampErrorResolver.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampErrorResolver.kt @@ -5,9 +5,9 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.domain.onramp.model.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver -internal class DefaultOnrampErrorResolver( - private val onrampErrorConverter: OnrampErrorConverter, -) : OnrampErrorResolver { +internal class DefaultOnrampErrorResolver : OnrampErrorResolver { + + private val onrampErrorConverter = OnrampErrorConverter() override fun resolve(throwable: Throwable): OnrampError { return if (throwable is ApiResponseError.HttpException) { 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..4145803c20 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 @@ -1,22 +1,42 @@ package com.tangem.data.onramp +import com.tangem.data.common.api.safeApiCall import com.tangem.data.onramp.converters.CountryConverter import com.tangem.data.onramp.converters.CurrencyConverter +import com.tangem.data.onramp.converters.StatusConverter +import com.tangem.data.onramp.converters.PaymentMethodConverter 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.onramp.OnrampPaymentMethodsStore +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.model.OnrampStatus +import com.tangem.domain.onramp.model.OnrampPaymentMethod 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 +import timber.log.Timber internal class DefaultOnrampRepository( private val onrampApi: OnrampApi, private val dispatchers: CoroutineDispatcherProvider, + private val appPreferencesStore: AppPreferencesStore, + private val paymentMethodsStore: OnrampPaymentMethodsStore, ) : OnrampRepository { private val currencyConverter = CurrencyConverter() - private val countryConverter = CountryConverter() + private val countryConverter = CountryConverter(currencyConverter) + private val statusConverter = StatusConverter() + private val paymentMethodsConverter = PaymentMethodConverter() override suspend fun getCurrencies(): List = withContext(dispatchers.io) { onrampApi.getCurrencies() @@ -29,4 +49,78 @@ internal class DefaultOnrampRepository( .getOrThrow() .map(countryConverter::convert) } + + override suspend fun getCountryByIp(): OnrampCountry = withContext(dispatchers.io) { + onrampApi.getCountryByIp() + .getOrThrow() + .let(countryConverter::convert) + } + + override suspend fun getStatus(txId: String): OnrampStatus = withContext(dispatchers.io) { + onrampApi.getStatus(txId) + .getOrThrow() + .let(statusConverter::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) } + } + + override suspend fun fetchPaymentMethodsIfAbsent() { + if (paymentMethodsStore.contains(PAYMENT_METHODS_KEY)) return + + val response = safeApiCall( + call = { onrampApi.getPaymentMethods().bind() }, + onError = { + Timber.w(it, "Unable to fetch onramp payment methods") + throw it + }, + ) + paymentMethodsStore.store(PAYMENT_METHODS_KEY, response) + } + + override suspend fun getPaymentMethods(): List { + val paymentMethods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) { + "Onramp payment methods is absent in storage" + } + return paymentMethodsConverter.convertList(paymentMethods) + } + + private companion object { + const val PAYMENT_METHODS_KEY = "onramp_payment_methods" + } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt new file mode 100644 index 0000000000..eaaad04a78 --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt @@ -0,0 +1,77 @@ +package com.tangem.data.onramp + +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSet +import com.tangem.datasource.local.preferences.utils.getObjectSetSync +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +internal class DefaultOnrampTransactionRepository( + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : OnrampTransactionRepository { + + override suspend fun storeTransaction(transaction: OnrampTransaction) { + withContext(dispatchers.io) { + appPreferencesStore.editData { mutablePreferences -> + val stored = mutablePreferences.getObjectSet( + PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY, + ) + val updated = stored?.toMutableSet() + ?.addOrReplace(transaction) { it.txId == transaction.txId } + ?: mutableSetOf(transaction) + + mutablePreferences.setObjectSet( + key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY, + value = updated, + ) + } + } + } + + override suspend fun getTransactionById(txId: String): OnrampTransaction? = withContext(dispatchers.io) { + val stored = appPreferencesStore.getObjectSetSync( + PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY, + ) + + stored.firstOrNull { it.txId == txId } + } + + override fun getTransactions( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + ): Flow> = appPreferencesStore + .getObjectSet(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY) + .map { transactions -> + transactions.filter { + it.userWalletId == userWalletId && it.toCurrencyId == cryptoCurrencyId.value + } + } + + override suspend fun removeTransaction(txId: String) { + withContext(dispatchers.io) { + appPreferencesStore.editData { mutablePreferences -> + runCatching { + val stored = mutablePreferences.getObjectSet( + PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY, + )?.toMutableSet() + + stored?.removeIf { it.txId == txId } + + mutablePreferences.setObjectSet( + key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY, + value = stored ?: emptySet(), + ) + } + } + } + } +} \ 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/converters/PaymentMethodConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/PaymentMethodConverter.kt new file mode 100644 index 0000000000..b45273c12e --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/PaymentMethodConverter.kt @@ -0,0 +1,13 @@ +package com.tangem.data.onramp.converters + +import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.utils.converter.Converter + +internal class PaymentMethodConverter : Converter { + override fun convert(value: PaymentMethodDTO): OnrampPaymentMethod = OnrampPaymentMethod( + id = value.id, + name = value.name, + imageUrl = value.image, + ) +} \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/StatusConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/StatusConverter.kt new file mode 100644 index 0000000000..70805a77fe --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/StatusConverter.kt @@ -0,0 +1,30 @@ +package com.tangem.data.onramp.converters + +import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse +import com.tangem.domain.onramp.model.OnrampStatus +import com.tangem.utils.converter.Converter + +internal class StatusConverter : Converter { + override fun convert(value: OnrampStatusResponse): OnrampStatus { + return OnrampStatus( + txId = value.txId, + providerId = value.providerId, + payoutAddress = value.payoutAddress, + status = OnrampStatus.Status.valueOf(value.status.name), + failReason = value.failReason, + externalTxId = value.externalTxId, + externalTxUrl = value.externalTxUrl, + payoutHash = value.payoutHash, + createdAt = value.createdAt, + fromCurrencyCode = value.fromCurrencyCode, + fromAmount = value.fromAmount, + toContractAddress = value.toContractAddress, + toNetwork = value.toNetwork, + toDecimals = value.toDecimals, + toAmount = value.toAmount, + toActualAmount = value.toActualAmount, + paymentMethod = value.paymentMethod, + countryCode = value.countryCode, + ) + } +} \ 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..3731a0773d 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 @@ -1,8 +1,14 @@ package com.tangem.data.onramp.di +import com.tangem.data.onramp.DefaultOnrampErrorResolver import com.tangem.data.onramp.DefaultOnrampRepository +import com.tangem.data.onramp.DefaultOnrampTransactionRepository import com.tangem.datasource.api.onramp.OnrampApi +import com.tangem.datasource.local.onramp.OnrampPaymentMethodsStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -16,10 +22,35 @@ internal object OnrampDataModule { @Provides @Singleton - fun provideOnrampRepository(onrampApi: OnrampApi, dispatchers: CoroutineDispatcherProvider): OnrampRepository { + fun provideOnrampRepository( + onrampApi: OnrampApi, + dispatchers: CoroutineDispatcherProvider, + appPreferencesStore: AppPreferencesStore, + paymentMethodsStore: OnrampPaymentMethodsStore, + ): OnrampRepository { return DefaultOnrampRepository( onrampApi = onrampApi, dispatchers = dispatchers, + appPreferencesStore = appPreferencesStore, + paymentMethodsStore = paymentMethodsStore, ) } + + @Provides + @Singleton + fun provideOnrampTransactionRepository( + appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, + ): OnrampTransactionRepository { + return DefaultOnrampTransactionRepository( + appPreferencesStore = appPreferencesStore, + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun provideOnrampErrorResolver(): OnrampErrorResolver { + return DefaultOnrampErrorResolver() + } } \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index fbd89738aa..0a81c2200c 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -23,15 +23,6 @@ internal class DefaultPromoRepository( }.getOrNull() } - override suspend fun getTravalaPromoBanner(): PromoBanner? { - return runCatching(dispatchers.io) { - promoResponseConverter.convert( - tangemApi.getPromotionInfo(TRAVALA) - .getOrThrow(), - ) - }.getOrNull() - } - override suspend fun getOkxPromoBanner(): PromoBanner? { return runCatching(dispatchers.io) { promoResponseConverter.convert( @@ -52,7 +43,6 @@ internal class DefaultPromoRepository( private companion object { private const val CHANGELLY_NAME = "changelly" - private const val TRAVALA = "travala" private const val OKX = "okx" private const val RING = "ring" } diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt index ecd647ae73..8a2b5aa1d9 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt @@ -4,7 +4,6 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY -import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_TRAVALA_PROMO_SHOWN_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.store @@ -42,17 +41,6 @@ class DefaultPromoSettingsRepository( ) } - override fun isReadyToShowWalletTravalaPromo(): Flow { - return appPreferencesStore.get(IS_WALLET_TRAVALA_PROMO_SHOWN_KEY, true) - } - - override suspend fun setNeverToShowWalletTravalaPromo() { - appPreferencesStore.store( - key = IS_WALLET_TRAVALA_PROMO_SHOWN_KEY, - value = false, - ) - } - override fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow { return combine( flow = appPreferencesStore 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 0e66a0da16..be7dc4d12e 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 @@ -49,7 +49,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 @@ -68,7 +67,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, @@ -323,8 +321,6 @@ internal class DefaultStakingRepository( cryptoCurrency: CryptoCurrency, refresh: Boolean, ) = withContext(dispatchers.io) { - if (!stakingFeatureToggle.isStakingEnabled) return@withContext - cacheRegistry.invokeOnExpire( key = getYieldBalancesKey(userWalletId), skipCache = refresh, @@ -364,30 +360,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() @@ -395,21 +387,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( @@ -417,8 +405,6 @@ internal class DefaultStakingRepository( cryptoCurrencies: List, refresh: Boolean, ) = withContext(dispatchers.io) { - if (!stakingFeatureToggle.isStakingEnabled) return@withContext - cacheRegistry.invokeOnExpire( key = getYieldBalancesKey(userWalletId), skipCache = refresh, @@ -484,19 +470,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) } } @@ -504,13 +486,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/build.gradle.kts b/data/tokens/build.gradle.kts index 7585dedc01..bf90a79e76 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.staking.models) /** Project - Data */ implementation(projects.core.datasource) 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..1d25e1ea81 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 { @@ -298,23 +293,28 @@ internal class DefaultCurrenciesRepository( userWalletId: UserWalletId, id: CryptoCurrency.ID, ): CryptoCurrency = withContext(dispatchers.io) { - val userWallet = getUserWallet(userWalletId) - ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) - - val response = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - }, - ) - - responseCurrenciesFactory.createCurrency( - currencyId = id, - response = response, - scanResponse = userWallet.scanResponse, - ) + getMultiCurrencyWalletCurrency(userWalletId, id.value) } + override suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency = + withContext(dispatchers.io) { + val userWallet = getUserWallet(userWalletId) + ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) + + val response = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + }, + ) + + responseCurrenciesFactory.createCurrency( + currencyId = id, + response = response, + scanResponse = userWallet.scanResponse, + ) + } + override suspend fun getNetworkCoin( userWalletId: UserWalletId, networkId: Network.ID, @@ -565,27 +565,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/DefaultCurrencyChecksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt index a62d70a3f8..afa7be79cc 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt @@ -6,13 +6,17 @@ import com.tangem.blockchain.common.MinimumSendAmountProvider import com.tangem.blockchain.common.ReserveAmountProvider import com.tangem.blockchain.common.UtxoAmountLimitProvider import com.tangem.data.tokens.converters.UtxoConverter +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.isZero import kotlinx.coroutines.withContext import java.math.BigDecimal @@ -118,4 +122,23 @@ internal class DefaultCurrencyChecksRepository( } return utxoAmount?.let(UtxoConverter()::convert) } + + override suspend fun getRentInfoWarning( + userWalletId: UserWalletId, + currencyStatus: CryptoCurrencyStatus, + balanceAfterTransaction: BigDecimal?, + ): CryptoCurrencyWarning.Rent? { + val rentData = walletManagersFacade.getRentInfo(userWalletId, currencyStatus.currency.network) ?: return null + val balanceValue = currencyStatus.value as? CryptoCurrencyStatus.Loaded ?: return null + val stakingTotalBalance = + (balanceValue.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance() ?: BigDecimal.ZERO + val amount = balanceAfterTransaction ?: balanceValue.amount + return when { + amount.isZero() && stakingTotalBalance.isZero() -> null + amount < rentData.exemptionAmount && stakingTotalBalance.isZero() -> { + CryptoCurrencyWarning.Rent(rentData.rent, rentData.exemptionAmount) + } + else -> null + } + } } \ No newline at end of file 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/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 5a531976c7..a684f579c9 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -25,11 +25,11 @@ import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState +import com.tangem.domain.walletmanager.model.RentData import com.tangem.domain.walletmanager.model.SmartContractMethod import com.tangem.domain.walletmanager.model.TokenInfo import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult @@ -413,7 +413,7 @@ class DefaultWalletManagersFacade( return manager?.wallet?.addresses.orEmpty() } - override suspend fun getRentInfo(userWalletId: UserWalletId, network: Network): CryptoCurrencyWarning.Rent? { + override suspend fun getRentInfo(userWalletId: UserWalletId, network: Network): RentData? { val manager = getOrCreateWalletManager( userWalletId = userWalletId, network = network, @@ -422,24 +422,7 @@ class DefaultWalletManagersFacade( return when (val result = manager.minimalBalanceForRentExemption()) { is Result.Success -> { - val balance = manager.wallet.fundsAvailable(AmountType.Coin) - val outgoingTxs = manager.wallet.recentTransactions - .filter { - it.sourceAddress == manager.wallet.address && - it.amount.type == AmountType.Coin && - it.status != TransactionStatus.Confirmed - } - - val rentExempt = result.data - val setRent = if (outgoingTxs.isEmpty()) { - balance < rentExempt - } else { - val outgoingAmount = outgoingTxs.sumOf { it.amount.value ?: BigDecimal.ZERO } - val rest = balance.minus(outgoingAmount) - balance < rest - } - - if (setRent) CryptoCurrencyWarning.Rent(manager.rentAmount(), rentExempt) else null + RentData(manager.rentAmount(), result.data) } is Result.Failure -> null } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 2f6317a316..6031b2300b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -12,11 +12,11 @@ import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState +import com.tangem.domain.walletmanager.model.RentData import com.tangem.domain.walletmanager.model.TokenInfo import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId @@ -140,7 +140,7 @@ interface WalletManagersFacade { * @param userWalletId selected wallet id * @param network network of currency */ - suspend fun getRentInfo(userWalletId: UserWalletId, network: Network): CryptoCurrencyWarning.Rent? + suspend fun getRentInfo(userWalletId: UserWalletId, network: Network): RentData? @Deprecated("Will be removed in future") fun getAll(userWalletId: UserWalletId): Flow> diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/RentData.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/RentData.kt new file mode 100644 index 0000000000..05973c0c00 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/RentData.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.walletmanager.model + +import java.math.BigDecimal + +/** + * Represents wallet blockchain rent + * @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than + * the [exemptionAmount] + * @param exemptionAmount Amount that should be on the blockchain balance not to pay rent + */ +data class RentData(val rent: BigDecimal, val exemptionAmount: BigDecimal) \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt index f8f62337a0..97a9008aa1 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt @@ -1,5 +1,6 @@ package com.tangem.domain.markets +import org.joda.time.DateTime import java.math.BigDecimal data class TokenMarketInfo( @@ -12,6 +13,7 @@ data class TokenMarketInfo( val fullDescription: String?, val insights: Insights?, val metrics: Metrics?, + val securityData: SecurityData?, val links: Links?, val pricePerformance: PricePerformance?, val exchangesAmount: Int?, @@ -51,6 +53,25 @@ data class TokenMarketInfo( val fullyDilutedValuation: BigDecimal?, ) + data class SecurityData( + val totalSecurityScore: Float, + val securityScoreProviderData: List, + ) + + data class SecurityScoreProvider( + val providerId: String, + val providerName: String, + val urlData: UrlData?, + val iconUrl: String, + val securityScore: Float, + val lastAuditDate: DateTime?, + ) { + data class UrlData( + val fullUrl: String, + val rootHost: String?, + ) + } + data class Links( val officialLinks: List?, val social: List?, diff --git a/domain/onramp/build.gradle.kts b/domain/onramp/build.gradle.kts index 43956b9497..b676e92120 100644 --- a/domain/onramp/build.gradle.kts +++ b/domain/onramp/build.gradle.kts @@ -6,6 +6,8 @@ plugins { dependencies { api(projects.domain.onramp.models) + api(projects.domain.tokens.models) + api(projects.domain.wallets.models) api(projects.domain.core) implementation(deps.kotlin.serialization) diff --git a/domain/onramp/models/build.gradle.kts b/domain/onramp/models/build.gradle.kts index 7558c5ca61..dc13a99957 100644 --- a/domain/onramp/models/build.gradle.kts +++ b/domain/onramp/models/build.gradle.kts @@ -6,6 +6,8 @@ plugins { dependencies { implementation(projects.domain.core) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) implementation(deps.kotlin.serialization) implementation(deps.jodatime) 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..601907de1a --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAvailability.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.onramp.model + +sealed interface OnrampAvailability { + + val country: OnrampCountry + + data class Available(override val country: OnrampCountry, val currency: OnrampCurrency) : OnrampAvailability + data class NotSupported(override val country: OnrampCountry) : OnrampAvailability + data class ConfirmResidency(override 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/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethod.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethod.kt new file mode 100644 index 0000000000..0ddf36dbf6 --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampPaymentMethod.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.onramp.model + +import kotlinx.serialization.Serializable + +@Serializable +data class OnrampPaymentMethod( + val id: String, + val name: String, + val imageUrl: String, +) \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt new file mode 100644 index 0000000000..69ecefb695 --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt @@ -0,0 +1,54 @@ +package com.tangem.domain.onramp.model + +import kotlinx.serialization.Serializable + +@Serializable +data class OnrampStatus( + val txId: String, + val providerId: String, + val payoutAddress: String, + val status: Status, + val failReason: String?, + val externalTxId: String, + val externalTxUrl: String?, + val payoutHash: String?, + val createdAt: String, + val fromCurrencyCode: String, + val fromAmount: String, + val toContractAddress: String, + val toNetwork: String, + val toDecimals: String, + val toAmount: String?, + val toActualAmount: String?, + val paymentMethod: String, + val countryCode: String, +) { + enum class Status(val order: Int) { + Created(order = 1), + Expired(order = 2), + Paused(order = 3), + WaitingForPayment(order = 4), + PaymentProcessing(order = 5), + Verifying(order = 6), + Failed(order = 7), + Paid(order = 8), + Sending(order = 9), + Finished(order = 10), + ; + + fun isTerminal(): Boolean = when (this) { + Expired, + Failed, + Paused, + Finished, + -> true + Created, + WaitingForPayment, + PaymentProcessing, + Verifying, + Paid, + Sending, + -> false + } + } +} \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt new file mode 100644 index 0000000000..96f069b02f --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.onramp.model.cache + +import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +data class OnrampTransaction( + val txId: String, + val userWalletId: UserWalletId, + val fromAmount: SerializedBigDecimal, + val fromCurrency: OnrampCurrency, + val toAmount: SerializedBigDecimal, + val toCurrencyId: String, + val providerName: String, + val providerImageUrl: String, +) \ No newline at end of file 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..2e099164d9 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt @@ -0,0 +1,28 @@ +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 { + repository.fetchPaymentMethodsIfAbsent() + repository.getDefaultCountrySync()?.let { savedCountry -> + return@catch if (savedCountry.onrampAvailable) { + val currency = repository.getDefaultCurrencySync() ?: run { + repository.saveDefaultCurrency(savedCountry.defaultCurrency) + savedCountry.defaultCurrency + } + OnrampAvailability.Available(country = savedCountry, currency = currency) + } 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/GetOnrampCurrencyUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrencyUseCase.kt new file mode 100644 index 0000000000..d2fc5147d1 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrencyUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.onramp.repositories.OnrampRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +class GetOnrampCurrencyUseCase(private val repository: OnrampRepository) { + + operator fun invoke(): Flow> { + return repository.getDefaultCurrency() + .map> { it.right() } + .catch { emit(it.left()) } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampPaymentMethodsUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampPaymentMethodsUseCase.kt new file mode 100644 index 0000000000..898bf3be19 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampPaymentMethodsUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.repositories.OnrampRepository + +class GetOnrampPaymentMethodsUseCase(private val repository: OnrampRepository) { + + suspend operator fun invoke(): Either> { + return Either.catch { repository.getPaymentMethods() } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampStatusUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampStatusUseCase.kt new file mode 100644 index 0000000000..4692c6d6b7 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampStatusUseCase.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampError +import com.tangem.domain.onramp.model.OnrampStatus +import com.tangem.domain.onramp.repositories.OnrampErrorResolver +import com.tangem.domain.onramp.repositories.OnrampRepository + +class GetOnrampStatusUseCase( + private val onrampRepository: OnrampRepository, + private val errorResolver: OnrampErrorResolver, +) { + + suspend operator fun invoke(txId: String): Either { + return Either.catch { + onrampRepository.getStatus(txId) + }.mapLeft { + errorResolver.resolve(it) + } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt new file mode 100644 index 0000000000..0e33b2e91a --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampError +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.onramp.repositories.OnrampTransactionRepository + +class GetOnrampTransactionUseCase( + private val onrampTransactionRepository: OnrampTransactionRepository, +) { + + suspend operator fun invoke(txId: String): Either { + return Either.catch { + requireNotNull( + onrampTransactionRepository.getTransactionById(txId), + ) + }.mapLeft { + OnrampError.UnknownError + } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionsUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionsUseCase.kt new file mode 100644 index 0000000000..af51320013 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionsUseCase.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampError +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +class GetOnrampTransactionsUseCase( + private val onrampTransactionRepository: OnrampTransactionRepository, +) { + + operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + ): Either>> { + return Either.catch { + onrampTransactionRepository.getTransactions( + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrencyId, + ) + }.mapLeft { + OnrampError.UnknownError + } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt new file mode 100644 index 0000000000..4a6062ece8 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampRemoveTransactionUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampError +import com.tangem.domain.onramp.repositories.OnrampTransactionRepository + +class OnrampRemoveTransactionUseCase( + private val onrampTransactionRepository: OnrampTransactionRepository, +) { + + suspend operator fun invoke(txId: String): Either { + return Either.catch { + onrampTransactionRepository.removeTransaction(txId) + }.mapLeft { + OnrampError.UnknownError + } + } +} \ 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/OnrampSaveTransactionUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveTransactionUseCase.kt new file mode 100644 index 0000000000..d939933d9c --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveTransactionUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampError +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.onramp.repositories.OnrampTransactionRepository + +class OnrampSaveTransactionUseCase( + private val onrampTransactionRepository: OnrampTransactionRepository, +) { + + suspend operator fun invoke(transaction: OnrampTransaction): Either { + return Either.catch { onrampTransactionRepository.storeTransaction(transaction) } + .mapLeft { OnrampError.UnknownError } + } +} \ 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..1c269c1a75 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,24 @@ package com.tangem.domain.onramp.repositories import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.onramp.model.OnrampStatus +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import kotlinx.coroutines.flow.Flow interface OnrampRepository { + // api suspend fun getCurrencies(): List suspend fun getCountries(): List + suspend fun getCountryByIp(): OnrampCountry + suspend fun getStatus(txId: String): OnrampStatus + suspend fun fetchPaymentMethodsIfAbsent() + + // cache + suspend fun saveDefaultCurrency(currency: OnrampCurrency) + suspend fun getDefaultCurrencySync(): OnrampCurrency? + fun getDefaultCurrency(): Flow + suspend fun saveDefaultCountry(country: OnrampCountry) + suspend fun getDefaultCountrySync(): OnrampCountry? + fun getDefaultCountry(): Flow + suspend fun getPaymentMethods(): List } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt new file mode 100644 index 0000000000..24105314b1 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampTransactionRepository.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.onramp.repositories + +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +interface OnrampTransactionRepository { + + suspend fun storeTransaction(transaction: OnrampTransaction) + + suspend fun getTransactionById(txId: String): OnrampTransaction? + + fun getTransactions(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow> + + suspend fun removeTransaction(txId: String) +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowTravalaPromoWalletUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowTravalaPromoWalletUseCase.kt deleted file mode 100644 index 643e7387c8..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowTravalaPromoWalletUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.domain.settings - -import com.tangem.domain.settings.repositories.PromoSettingsRepository -import kotlinx.coroutines.flow.Flow - -class ShouldShowTravalaPromoWalletUseCase(private val promoSettingsRepository: PromoSettingsRepository) { - - operator fun invoke(): Flow = promoSettingsRepository.isReadyToShowWalletTravalaPromo() - - suspend fun neverToShow() = promoSettingsRepository.setNeverToShowWalletTravalaPromo() -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt index 758763828a..b68a740e09 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt @@ -12,10 +12,6 @@ interface PromoSettingsRepository { suspend fun setNeverToShowTokenSwapPromo() - fun isReadyToShowWalletTravalaPromo(): Flow - - suspend fun setNeverToShowWalletTravalaPromo() - fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow suspend fun setNeverToShowRingPromo() diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt index 6242d8bbc2..70f2a8632b 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt @@ -37,7 +37,6 @@ sealed class TokenSwapPromoAnalyticsEvent( } enum class ProgramName { - Travala, OKX, Ring, } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt index dae8238b02..906c1ff0a4 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt @@ -10,4 +10,5 @@ data class CryptoCurrencyCheck( val existentialDeposit: BigDecimal?, val utxoAmountLimit: UtxoAmountLimit?, val isAccountFunded: Boolean, + val rentWarning: CryptoCurrencyWarning.Rent?, ) \ 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/GetCryptoCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt index d0e47c2b62..b24eb525ae 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt @@ -27,6 +27,22 @@ class GetCryptoCurrencyUseCase( return either { getCurrency(userWalletId, id) } } + /** + * Returns specific cryptocurrency for a given user wallet. + * + * !!! Important Use only [CryptoCurrency.ID.value] as cryptoCurrencyId + * + * @param userWalletId The ID of the user's wallet. + * @param cryptoCurrencyId String representation of the [CryptoCurrency.ID.value] of the cryptocurrency. + * @return An [Either] representing success (Right) or an error (Left) in fetching the status. + */ + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrencyId: String, + ): Either { + return either { getCurrency(userWalletId, cryptoCurrencyId) } + } + /** * Returns the primary cryptocurrency for a given user wallet. * @@ -49,6 +65,18 @@ class GetCryptoCurrencyUseCase( ) } + private suspend fun Raise.getCurrency( + userWalletId: UserWalletId, + id: String, + ): CryptoCurrency { + return catch( + block = { + currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) + }, + catch = { raise(CurrencyStatusError.DataError(it)) }, + ) + } + private suspend fun Raise.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { return catch( block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt index ba9e837248..8de4b3ea7e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt @@ -18,6 +18,7 @@ class GetCurrencyCheckUseCase( currencyStatus: CryptoCurrencyStatus, amount: BigDecimal?, fee: BigDecimal?, + balanceAfterTransaction: BigDecimal?, recipientAddress: String? = null, ): CryptoCurrencyCheck { return withContext(dispatchers.io) { @@ -26,6 +27,8 @@ class GetCurrencyCheckUseCase( val reserveAmount = currencyChecksRepository.getReserveAmount(userWalletId, network) val minimumSendAmount = currencyChecksRepository.getMinimumSendAmount(userWalletId, network) val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, network) + val rentWarning = + currencyChecksRepository.getRentInfoWarning(userWalletId, currencyStatus, balanceAfterTransaction) val isAccountFunded = recipientAddress?.let { currencyChecksRepository.checkIfAccountFunded( userWalletId, @@ -51,6 +54,7 @@ class GetCurrencyCheckUseCase( existentialDeposit = existentialDeposit, utxoAmountLimit = utxoAmountLimit, isAccountFunded = isAccountFunded, + rentWarning = rentWarning, ) } } 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..f52bcea470 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, @@ -57,7 +58,7 @@ class GetCurrencyWarningsUseCase( derivationPath = derivationPath, isSingleWalletWithTokens = isSingleWalletWithTokens, ), - flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), + flowOf(currencyChecksRepository.getRentInfoWarning(userWalletId, currencyStatus, null)), flowOf(currencyChecksRepository.getExistentialDeposit(userWalletId, currency.network)), flowOf(currencyChecksRepository.getFeeResourceAmount(userWalletId, currency.network)), getSwapPromoNotificationWarning( @@ -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/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index bda0e8fdbd..27691a8495 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -167,6 +167,17 @@ interface CurrenciesRepository { */ suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency + /** + * Retrieves the cryptocurrency for a specific multi-currency user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param id The unique identifier of the cryptocurrency to be retrieved. + * @return The cryptocurrency associated with the user wallet and ID. + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. + */ + suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency + /** * Get the coin for a specific network. * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt index 4ff37c6420..726e942b37 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt @@ -1,8 +1,10 @@ package com.tangem.domain.tokens.repository +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.wallets.models.UserWalletId import java.math.BigDecimal @@ -39,4 +41,14 @@ interface CurrencyChecksRepository { amount: BigDecimal, fee: BigDecimal, ): UtxoAmountLimit? + + /** + * Checks if need warning for Solana rent + * @param balanceAfterTransaction if non null, checks with balance remains after transaction + */ + suspend fun getRentInfoWarning( + userWalletId: UserWalletId, + currencyStatus: CryptoCurrencyStatus, + balanceAfterTransaction: BigDecimal?, + ): CryptoCurrencyWarning.Rent? } \ No newline at end of file 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/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt index 3cd5eacc17..39457c1113 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt @@ -6,8 +6,6 @@ interface PromoRepository { suspend fun getChangellyPromoBanner(): PromoBanner? - suspend fun getTravalaPromoBanner(): PromoBanner? - suspend fun getOkxPromoBanner(): PromoBanner? suspend fun getRingPromoBanner(): PromoBanner? diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index f68979c6f4..b23e4a8726 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -100,6 +100,14 @@ internal class MockCurrenciesRepository( return token } + override suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency { + val token = token.getOrElse { e -> throw e } + + require(token.id.value == id) + + return token + } + override suspend fun getNetworkCoin( userWalletId: UserWalletId, networkId: Network.ID, 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/analytics/MarketDetailsAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt index 3216b03dd3..85d934fd13 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt @@ -51,6 +51,19 @@ internal class MarketDetailsAnalyticsEvent( "Token" to token.symbol, ), ) + + fun securityScoreOpened() = MarketDetailsAnalyticsEvent( + event = "Security Score Info", + params = mapOf("Token" to token.symbol), + ) + + fun securityScoreProviderClicked(provider: String) = MarketDetailsAnalyticsEvent( + event = "Security Score Provider Clicked", + params = mapOf( + "Token" to token.symbol, + "Provider" to provider, + ), + ) } enum class IntervalType(val source: String) { 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..b6d4827fda 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 @@ -88,6 +88,20 @@ internal class MarketsTokenDetailsModel @Inject constructor( // === Analytics === analyticsEventHandler.send(analyticsEventBuilder.linkClicked(linkTitle = link.title)) }, + onSecurityScoreInfoClick = { + showBottomSheet(it) + + // === Analytics === + analyticsEventHandler.send(analyticsEventBuilder.securityScoreOpened()) + }, + onSecurityScoreProviderLinkClick = { + it.urlData?.fullUrl?.let { url -> + urlOpener.openUrl(url) + } + + // === Analytics === + analyticsEventHandler.send(analyticsEventBuilder.securityScoreProviderClicked(it.name)) + }, // === Analytics === onPricePerformanceIntervalChanged = { analyticsEventHandler.send( @@ -531,11 +545,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 Unit, -) : Converter { + private val onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit, + private val onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit, +) : Converter { - override fun convert(value: Unit): SecurityScoreUM { - return with(value) { - SecurityScoreUM( - score = 4.7f, - description = "Based on 3 ratings", - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_security_score), - body = stringReference("markets_token_details_security_score_description"), - // FIXME - // resourceReference(R.string.markets_token_details_security_score_description) - ), - ) - }, - ) - } + override fun convert(value: TokenMarketInfo.SecurityData): SecurityScoreUM { + val ratingsCount = value.securityScoreProviderData.size + return SecurityScoreUM( + score = value.totalSecurityScore, + description = pluralReference( + id = R.plurals.markets_token_details_based_on_ratings, + count = ratingsCount, + formatArgs = wrappedList(ratingsCount), + ), + onInfoClick = { + onSecurityScoreInfoClick( + SecurityScoreBottomSheetContent( + title = resourceReference(R.string.markets_token_details_security_score), + description = resourceReference(R.string.markets_token_details_security_score_description), + providers = value.securityScoreProviderData.map { + SecurityScoreBottomSheetContent.SecurityScoreProviderUM( + name = it.providerName, + lastAuditDate = it.lastAuditDate?.let { date -> + MarketsDateTimeFormatters.formatAsDate(date.millis) + }, + score = it.securityScore, + urlData = it.urlData?.let { + urlData -> + SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( + fullUrl = urlData.fullUrl, + rootHost = urlData.rootHost, + ) + }, + iconUrl = it.iconUrl, + ) + }, + onProviderLinkClick = onSecurityScoreProviderLinkClick, + ), + ) + }, + ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt index 3ab8df1094..85c354906a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt @@ -1,22 +1,26 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.LinksUM import com.tangem.features.markets.details.impl.ui.state.ListedOnUM import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM +import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent import com.tangem.utils.Provider import com.tangem.utils.converter.Converter @Stable +@Suppress("LongParameterList") internal class TokenMarketInfoConverter( private val appCurrency: Provider, - private val onInfoClick: (InfoBottomSheetContent) -> Unit, + private val onInfoClick: (TangemBottomSheetConfigContent) -> Unit, private val onListedOnClick: (Int) -> Unit, + onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit, onLinkClick: (LinksUM.Link) -> Unit, + onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit, onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit, onInsightsIntervalChanged: (PriceChangeInterval) -> Unit, ) : Converter { @@ -27,9 +31,10 @@ internal class TokenMarketInfoConverter( onIntervalChanged = onInsightsIntervalChanged, ) - @Suppress("UnusedPrivateMember") - // TODO second markets iteration - private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick) + private val securityScoreConverter = SecurityScoreConverter( + onSecurityScoreInfoClick = onSecurityScoreInfoClick, + onSecurityScoreProviderLinkClick = onSecurityScoreProviderLinkClick, + ) private val pricePerformanceConverter = PricePerformanceConverter( appCurrency = appCurrency, onIntervalChanged = onPricePerformanceIntervalChanged, @@ -46,7 +51,7 @@ internal class TokenMarketInfoConverter( val exchangesAmount = value.exchangesAmount return MarketsTokenDetailsUM.InformationBlocks( insights = value.insights?.let { insightsConverter.convert(it) }, - securityScore = null, + securityScore = value.securityData?.let { securityScoreConverter.convert(it) }, metrics = value.metrics?.let { metricsConverter.convert(it) }, pricePerformance = value.pricePerformance?.let { pricePerformanceConverter.convert( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt index 5abc296917..46ba611519 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt @@ -55,7 +55,7 @@ internal object MarketsDateTimeFormatters { R.string.common_range_with_space, wrappedList( stringReference( - startTimestamp.formatAsDateTime(MarketsDateTimeFormatters.dateTimeMMMFormatter), + startTimestamp.formatAsDateTime(dateTimeMMMFormatter), ), resourceReference(R.string.common_now), ), @@ -68,7 +68,7 @@ internal object MarketsDateTimeFormatters { R.string.common_range_with_space, wrappedList( stringReference( - startTimestamp.formatAsDateTime(MarketsDateTimeFormatters.dateFormatter), + startTimestamp.formatAsDateTime(dateFormatter), ), resourceReference(R.string.common_now), ), @@ -89,7 +89,7 @@ internal object MarketsDateTimeFormatters { R.string.common_range_with_space, wrappedList( stringReference( - markerTimestamp.toLong().formatAsDateTime(MarketsDateTimeFormatters.dateTimeMMMFormatter), + markerTimestamp.toLong().formatAsDateTime(dateTimeMMMFormatter), ), resourceReference(R.string.common_now), ), @@ -103,7 +103,7 @@ internal object MarketsDateTimeFormatters { R.string.common_range_with_space, wrappedList( stringReference( - markerTimestamp.toLong().formatAsDateTime(MarketsDateTimeFormatters.dateFormatter), + markerTimestamp.toLong().formatAsDateTime(dateFormatter), ), resourceReference(R.string.common_now), ), @@ -128,10 +128,14 @@ internal object MarketsDateTimeFormatters { fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference { return formatDateByInterval( interval = interval, - startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval( + startTimestamp = getStartTimestampByInterval( interval = interval, currentTimestamp = currentTimestamp, ), ) } + + fun formatAsDate(timestamp: Long): String { + return timestamp.formatAsDateTime(dateFormatter) + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index 4e46c48655..59c5d3d8f1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -37,6 +37,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.markets.details.impl.ui.components.* import com.tangem.features.markets.details.impl.ui.components.ExchangesBottomSheet import com.tangem.features.markets.details.impl.ui.components.InfoBottomSheet import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart @@ -44,6 +45,7 @@ import com.tangem.features.markets.details.impl.ui.components.tokenMarketDetails import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM +import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.persistentListOf @@ -72,6 +74,7 @@ internal fun MarketsTokenDetailsContent( when (state.bottomSheetConfig.content) { is InfoBottomSheetContent -> InfoBottomSheet(config = state.bottomSheetConfig) + is SecurityScoreBottomSheetContent -> SecurityScoreBottomSheet(config = state.bottomSheetConfig) is ExchangesBottomSheetContent -> ExchangesBottomSheet(config = state.bottomSheetConfig) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt index 5e2a7e2973..8a0040876c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt @@ -176,9 +176,9 @@ private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterPr isGrayscale = false, showCustomBadge = false, ), - titleState = TokenItemState.TitleState.Content(text = "OKX"), + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "OKX")), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "$67.52M"), - subtitleState = TokenItemState.SubtitleState.TextContent(value = "CEX"), + subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference(value = "CEX")), subtitle2State = TokenItemState.Subtitle2State.LabelContent( auditLabelUM = AuditLabelUM( text = stringReference("Caution"), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt new file mode 100644 index 0000000000..d7d047c38b --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt @@ -0,0 +1,96 @@ +package com.tangem.features.markets.details.impl.ui.components + +import androidx.annotation.FloatRange +import androidx.compose.foundation.layout.* +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.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.markets.impl.R +import kotlin.math.round + +private const val STARS_COUNT = 5 + +@Composable +internal fun ScoreStarsBlock( + score: Float, + horizontalSpacing: Dp, + scoreTextStyle: TextStyle, + modifier: Modifier = Modifier, +) { + val rounded = score.roundTo1decimal() + val percentage = rounded / STARS_COUNT + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(horizontalSpacing), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = rounded.toString(), + style = scoreTextStyle, + color = TangemTheme.colors.text.primary1, + ) + Stars(fraction = percentage) + } +} + +@Suppress("MagicNumber") +@Composable +private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) { + val grayColor = TangemTheme.colors.icon.inactive + + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(times = 5) { i -> + Box( + modifier = Modifier.size(TangemTheme.dimens.size16), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .requiredSize(16.dp) + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .drawWithCache { + onDrawWithContent { + val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0) + val starFractionFloat = starFraction + .toFloat() + .roundTo1decimal() + + drawContent() + drawRect( + color = grayColor, + topLeft = Offset(x = size.width * starFractionFloat, y = 0f), + size = Size(size.width * (1 - starFractionFloat), size.height), + blendMode = BlendMode.SrcIn, + ) + } + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_star_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + } + } + } +} + +@Suppress("MagicNumber") +private fun Float.roundTo1decimal(): Float { + return round(this * 10) / 10 +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt index 66a75660ef..468b7f7b59 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt @@ -1,197 +1,128 @@ package com.tangem.features.markets.details.impl.ui.components import android.content.res.Configuration -import androidx.annotation.FloatRange +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -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.drawWithCache -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.text.TooltipText +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.core.ui.utils.PreviewShimmerContainer import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM import com.tangem.features.markets.impl.R -import kotlin.math.round - -private const val STARS_COUNT = 5 @Composable internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) { - val rounded = state.score.roundTo1decimal() - val percentage = rounded / STARS_COUNT - InformationBlock( - modifier = modifier, - title = { - Column( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - TooltipText( - text = resourceReference(R.string.markets_token_details_security_score), - onInfoClick = state.onInfoClick, - textStyle = TangemTheme.typography.subtitle2, - ) - - Text( - text = state.description, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - }, - action = { - Row( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = rounded.toString(), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - Stars(fraction = percentage) - } - }, - ) -} - -@Suppress("MagicNumber") -@Composable -private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) { - val grayColor = TangemTheme.colors.icon.inactive - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .fillMaxWidth() + .heightIn(max = TangemTheme.dimens.size72) + .padding(all = TangemTheme.dimens.spacing12), verticalAlignment = Alignment.CenterVertically, ) { - repeat(times = 5) { i -> - Box( - modifier = Modifier.size(TangemTheme.dimens.size16), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .requiredSize(13.dp) - .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) - .drawWithCache { - onDrawWithContent { - val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0) - val starFractionFloat = starFraction - .toFloat() - .roundTo1decimal() + Column( + modifier = Modifier + .weight(1F) + .fillMaxHeight(), + verticalArrangement = Arrangement.SpaceBetween, + ) { + TooltipText( + text = resourceReference(R.string.markets_token_details_security_score), + onInfoClick = state.onInfoClick, + textStyle = TangemTheme.typography.subtitle2, + ) - drawContent() - drawRect( - color = grayColor, - topLeft = Offset(x = size.width * starFractionFloat, y = 0f), - size = Size(size.width * (1 - starFractionFloat), size.height), - blendMode = BlendMode.SrcIn, - ) - } - }, - imageVector = ImageVector.vectorResource(R.drawable.ic_star_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - } + Text( + text = state.description.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } + ScoreStarsBlock( + score = state.score, + scoreTextStyle = TangemTheme.typography.body1, + horizontalSpacing = TangemTheme.dimens.spacing8, + ) } } -@Suppress("MagicNumber") -private fun Float.roundTo1decimal(): Float { - return round(this * 10) / 10 -} - @Composable -internal fun SecurityScorePlaceHolder(modifier: Modifier = Modifier) { - InformationBlock( - modifier = modifier, - title = { - Column( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.subtitle2, - textSizeHeight = true, - ) - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } - }, - action = { - Box( - modifier = Modifier.padding( - start = TangemTheme.dimens.spacing24, - bottom = TangemTheme.dimens.spacing6, - ), - contentAlignment = Alignment.CenterEnd, - ) { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - RectangleShimmer( - modifier = Modifier - .height(TangemTheme.dimens.size16) - .fillMaxWidth(), - radius = TangemTheme.dimens.radius3, - ) - } - }, - ) +internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.primary) + .fillMaxWidth() + .heightIn(max = TangemTheme.dimens.size72) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column( + modifier = Modifier + .fillMaxWidth(fraction = 0.4f) + .padding(vertical = TangemTheme.dimens.spacing2) + .fillMaxHeight(), + verticalArrangement = Arrangement.SpaceBetween, + ) { + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.subtitle2, + textSizeHeight = true, + ) + TextShimmer( + modifier = Modifier.fillMaxWidth(), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + } + + TextShimmer( + modifier = Modifier.fillMaxWidth(fraction = 0.5f), + style = TangemTheme.typography.body2, + textSizeHeight = true, + ) + } } -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 328, showBackground = true) +@Preview(widthDp = 328, showBackground = true, locale = "ru") +@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ContentPreview() { TangemThemePreview { SecurityScoreBlock( state = SecurityScoreUM( score = 3.5f, - description = "Based on 3 ratings", + description = stringReference("Based on 3 ratings"), onInfoClick = {}, ), ) } } -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 328, showBackground = true) +@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun PreviewPlaceholder() { TangemThemePreview { PreviewShimmerContainer( shimmerContent = { - SecurityScorePlaceHolder( + SecurityScoreBlockPlaceholder( modifier = Modifier.fillMaxWidth(), ) }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt new file mode 100644 index 0000000000..07019d64e1 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt @@ -0,0 +1,190 @@ +package com.tangem.features.markets.details.impl.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.util.fastForEachIndexed +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.details.impl.ui.preview.SecurityScorePreviewData +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent + +@Composable +internal fun SecurityScoreBottomSheet(config: TangemBottomSheetConfig) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + TangemBottomSheet( + config = config, + addBottomInsets = false, + title = { TangemBottomSheetTitle(title = it.title) }, + content = { content -> + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) { + Text( + text = content.description.resolveReference(), + style = TangemTheme.typography.body2.copy( + color = TangemTheme.colors.text.secondary, + ), + ) + + SpacerH12() + content.providers.fastForEachIndexed { index, provider -> + DividerContainer( + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = content.providers.lastIndex, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action), + showDivider = index != content.providers.lastIndex, + ) { + SecurityScoreProviderRow( + providerUM = provider, + onLinkClick = content.onProviderLinkClick, + ) + } + } + + SpacerH16() + SpacerH(bottomBarHeight) + } + }, + ) +} + +@Composable +private fun SecurityScoreProviderRow( + providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM, + onLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing12) + .heightIn(min = TangemTheme.dimens.size68), + verticalAlignment = Alignment.CenterVertically, + ) { + SubcomposeAsyncImage( + modifier = Modifier + .size(size = TangemTheme.dimens.size40) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current) + .data(providerUM.iconUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + error = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + contentDescription = null, + ) + + Column( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + Text( + text = providerUM.name, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + providerUM.lastAuditDate?.let { + Text( + text = providerUM.lastAuditDate, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + + SpacerWMax() + + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + modifier = Modifier.clickable( + enabled = providerUM.urlData != null, + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onLinkClick(providerUM) }, + ), + ) { + ScoreStarsBlock( + score = providerUM.score, + scoreTextStyle = TangemTheme.typography.body2, + horizontalSpacing = TangemTheme.dimens.spacing3, + ) + + UrlBlock(providerUM) + } + } +} + +@Composable +private fun UrlBlock(providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM) { + val urlData = providerUM.urlData + val rootHost = urlData?.rootHost + if (urlData != null && rootHost != null) { + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + Text( + text = urlData.rootHost, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier + .size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_arrow_top_right_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SecurityScoreBottomSheetPreview() { + TangemThemePreview { + SecurityScoreBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = SecurityScorePreviewData.bottomSheetContent, + ), + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt index 521ced4a30..0d04d56bbe 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt @@ -116,7 +116,10 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati } item(key = "listedOn") { - ListedOnBlock(state = state.listedOn, modifier = Modifier.blockPaddings()) + ListedOnBlock( + state = state.listedOn, + modifier = Modifier.blockPaddings(), + ) } if (state.links != null) { @@ -134,10 +137,9 @@ private fun LazyListScope.loadingInfoBlocks() { InsightsBlockPlaceholder(modifier = Modifier.blockPaddings()) } - // TODO second markets iteration - // item("securityScore-loading") { - // SecurityScorePlaceHolder(modifier = Modifier.blockPaddings()) - // } + item("securityScore-loading") { + SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings()) + } item("metrics-loading") { MetricsBlockPlaceholder(modifier = Modifier.blockPaddings()) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt new file mode 100644 index 0000000000..d80e49af6c --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt @@ -0,0 +1,60 @@ +package com.tangem.features.markets.details.impl.ui.preview + +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent + +internal object SecurityScorePreviewData { + + val bottomSheetContent = SecurityScoreBottomSheetContent( + title = stringReference("Security score"), + description = stringReference( + "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.", + ), + providers = listOf( + SecurityScoreBottomSheetContent.SecurityScoreProviderUM( + name = "Moralis", + lastAuditDate = "21.10.2024", + score = 4.9F, + urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( + fullUrl = "https://moralis.com/", + rootHost = "moralis.com", + ), + iconUrl = "", + ), + SecurityScoreBottomSheetContent.SecurityScoreProviderUM( + name = "Certik", + lastAuditDate = "10.07.2024", + score = 4.6F, + urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( + fullUrl = "https://certik.com/", + rootHost = "certik.com", + ), + iconUrl = "", + ), + SecurityScoreBottomSheetContent.SecurityScoreProviderUM( + name = "Cyberscope", + lastAuditDate = "25.06.2023", + score = 4.5F, + urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( + fullUrl = "https://cyberscope.com/", + rootHost = "cyberscope.com", + ), + iconUrl = "", + ), + SecurityScoreBottomSheetContent.SecurityScoreProviderUM( + name = "TokenInsight", + lastAuditDate = "17.01.2022", + score = 4.0F, + urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( + fullUrl = "https://tokeninsight.com/", + rootHost = "tokeninsight.com", + ), + iconUrl = "", + ), + + ), + onProviderLinkClick = {}, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt new file mode 100644 index 0000000000..ceb100bdfb --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt @@ -0,0 +1,25 @@ +package com.tangem.features.markets.details.impl.ui.state + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference + +internal data class SecurityScoreBottomSheetContent( + val title: TextReference, + val description: TextReference, + val providers: List, + val onProviderLinkClick: (SecurityScoreProviderUM) -> Unit, +) : TangemBottomSheetConfigContent { + + data class SecurityScoreProviderUM( + val name: String, + val lastAuditDate: String?, + val score: Float, + val urlData: UrlData?, + val iconUrl: String?, + ) { + data class UrlData( + val fullUrl: String, + val rootHost: String?, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt index 35f35d4b9c..d4ebd3a9ee 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt @@ -1,9 +1,10 @@ package com.tangem.features.markets.details.impl.ui.state import androidx.annotation.FloatRange +import com.tangem.core.ui.extensions.TextReference internal data class SecurityScoreUM( @FloatRange(from = 0.0, to = 5.0) val score: Float, - val description: String, + val description: TextReference, val onInfoClick: () -> Unit, ) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index 1125fda4b1..946fcbdd07 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -2,6 +2,7 @@ package com.tangem.features.markets.portfolio.impl.model import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason @@ -29,11 +30,11 @@ internal class PortfolioTokenUMConverter( override fun convert(value: PortfolioData.CryptoCurrencyData): PortfolioTokenUM { val tokenItemStateConverter = TokenItemStateConverter( appCurrency = appCurrency, - titleStateProvider = { TokenItemState.TitleState.Content(text = value.userWallet.name) }, + titleStateProvider = { TokenItemState.TitleState.Content(text = stringReference(value.userWallet.name)) }, subtitleStateProvider = { - TokenItemState.SubtitleState.TextContent(value = value.status.currency.name) + TokenItemState.SubtitleState.TextContent(value = stringReference(value.status.currency.name)) }, - onItemClick = onTokenItemClick, + onItemClick = { _, status -> 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..112c1b01f6 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -23,11 +23,14 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.res) implementation(projects.core.decompose) + implementation(projects.core.navigation) /** Domain */ 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) @@ -49,11 +52,13 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.coil) implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) /** Other libraries */ 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..876f7de961 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,16 +4,36 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.FaultyDecomposeApi +import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.fade +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.arkivanov.essenty.instancekeeper.getOrCreateSimple 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.chooseoption.Wallet1ChooseOptionComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.MultiWalletCreateWalletComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.MultiWalletSeedPhraseComponent 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.model.OnboardingMultiWalletState.Step.* import com.tangem.features.onboarding.v2.multiwallet.impl.ui.OnboardingMultiWallet +import com.tangem.features.onboarding.v2.multiwallet.impl.ui.WalletArtworksState import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor( @@ -22,24 +42,147 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor ) : OnboardingMultiWalletComponent, AppComponentContext by context { private val model: OnboardingMultiWalletModel = getOrCreateModel(params) + private val stackNavigation = StackNavigation() + private val artworksState = instanceKeeper.getOrCreateSimple { + MutableStateFlow( + when (model.state.value.currentStep) { + CreateWallet -> WalletArtworksState.Folded + ChooseBackupOption -> WalletArtworksState.Fan + SeedPhrase -> WalletArtworksState.Folded + AddBackupDevice -> WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.First) + FinishBackup -> WalletArtworksState.Stack() + Done -> error("Done state should not be used as starting state") + }, + ) + } 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) { + CreateWallet -> MultiWalletCreateWalletComponent( + context = childContext, + params = MultiWalletChildParams( + multiWalletState = model.state, + parentParams = params, + ), + onNextStep = ::handleNavigationEvent, + ) + ChooseBackupOption -> Wallet1ChooseOptionComponent( + context = childContext, + onNextStep = ::handleNavigationEvent, + ) + SeedPhrase -> MultiWalletSeedPhraseComponent( + context = childContext, + params = MultiWalletChildParams( + multiWalletState = model.state, + parentParams = params, + ), + onNextStep = ::handleNavigationEvent, + backButtonClickFlow = MutableSharedFlow(), + ) + AddBackupDevice -> MultiWalletBackupComponent( + context = childContext, + params = MultiWalletChildParams( + multiWalletState = model.state, + parentParams = params, + ), + onEvent = ::handleBackupComponentEvent, + ) + FinishBackup -> TODO() + Done -> TODO() + } + } + + fun handleNavigationEvent(nextStep: OnboardingMultiWalletState.Step) { + when (nextStep) { + ChooseBackupOption -> { + artworksState.value = WalletArtworksState.Fan + } + SeedPhrase -> { + } + AddBackupDevice -> { + artworksState.value = WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.First) + } + FinishBackup -> { + } + Done -> { + // TODO + } + else -> return + } + stackNavigation.push(nextStep) + } + + fun handleBackupComponentEvent(event: MultiWalletBackupComponent.Event) { + when (event) { + MultiWalletBackupComponent.Event.Done -> { + // TODO navigate to finalize + } + MultiWalletBackupComponent.Event.OneDeviceAdded -> { + artworksState.value = WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.Second) + } + MultiWalletBackupComponent.Event.TwoDeviceAdded -> { + artworksState.value = WalletArtworksState.Unfold(WalletArtworksState.Unfold.Step.Third) + } + } + } + + @OptIn(FaultyDecomposeApi::class) @Composable override fun Content(modifier: Modifier) { - val uiState by model.uiState.collectAsStateWithLifecycle() + val stackState by childStack.subscribeAsState() + val state by model.uiState.collectAsStateWithLifecycle() + val artworksState by artworksState.collectAsStateWithLifecycle() OnboardingMultiWallet( + state = state, modifier = modifier, - state = uiState, + artworksState = artworksState, + isSeedPhraseState = stackState.active.configuration == SeedPhrase, + childContent = { mdfr -> + Children( + stack = stackState, + animation = stackAnimation( + selector = { one, two, direction -> + if (one.configuration == SeedPhrase || two.configuration == SeedPhrase) { + fade() + } else { + slide() + } + }, + ), + ) { + it.instance.Content(mdfr) + } + }, ) } @@ -52,7 +195,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..a60cfcb3fa --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/MultiWalletBackupComponent.kt @@ -0,0 +1,49 @@ +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, + onEvent: (event: Event) -> 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.eventFlow.collect(onEvent) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + MultiWalletBackup( + modifier = modifier, + state = state, + ) + } + + enum class Event { + Done, OneDeviceAdded, TwoDeviceAdded + } +} \ 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..59ea93d2be --- /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,192 @@ +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.MultiWalletBackupComponent +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, + ProductType.Wallet2, + ProductType.Ring, + -> getInitState() + else -> error("Type: ${scanResponse.productType.name} is not supported!") + }, + ) + + val uiState: StateFlow = _uiState + val eventFlow = MutableSharedFlow() + + private fun getInitState(): MultiWalletBackupUM { + return when (backupService.currentState) { + BackupService.State.Preparing -> { + MultiWalletBackupUM( + title = resourceReference(R.string.onboarding_title_no_backup_cards), + bodyText = resourceReference(R.string.onboarding_subtitle_no_backup_cards), + finalizeButtonEnabled = false, + addBackupButtonEnabled = true, + addBackupButtonLoading = false, + onAddBackupClick = ::startBackupWallet, + onFinalizeButtonClick = {}, + onSkipButtonClick = { + // eventFlow.tryEmit(Unit) + }, + ) + } + is BackupService.State.FinalizingBackupCard -> TODO() + BackupService.State.FinalizingPrimaryCard -> TODO() + BackupService.State.Finished -> TODO() + } + } + + private fun startBackupWallet() { + backupService.discardSavedBackup() + val primaryCard = scanResponse.primaryCard + + if (primaryCard != null) { + backupService.setPrimaryCard(primaryCard) + addBackupCardWithService() + } else { + // TODO wallet 2 ??? or from main? + // BackupAction.StartAddingPrimaryCard + } + } + + private fun setNumberOfBackupCards(number: Int) { + // set state for adding backup cards and disable button if there is more than 2 backup cards + _uiState.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), + addBackupButtonEnabled = true, + ) + } + 1 -> { + modelScope.launch { + eventFlow.emit(MultiWalletBackupComponent.Event.OneDeviceAdded) + } + st.copy( + title = resourceReference(R.string.onboarding_title_one_backup_card), + bodyText = resourceReference(R.string.onboarding_subtitle_one_backup_card), + addBackupButtonEnabled = true, + finalizeButtonEnabled = true, + ) + } + 2 -> { + modelScope.launch { + eventFlow.emit(MultiWalletBackupComponent.Event.TwoDeviceAdded) + } + st.copy( + title = resourceReference(R.string.onboarding_title_two_backup_cards), + bodyText = resourceReference(R.string.onboarding_subtitle_two_backup_cards), + addBackupButtonEnabled = false, + finalizeButtonEnabled = true, + ) + } + else -> st.copy(addBackupButtonEnabled = false) + } + } + } + + private fun addBackupCardWithService() { + _uiState.update { st -> + st.copy(addBackupButtonLoading = true) + } + + backupService.addBackupCard { result -> + backupService.skipCompatibilityChecks = false + cardSdkConfigRepository.sdk.config.filter.cardIdFilter = null + + _uiState.update { st -> + st.copy(addBackupButtonLoading = false) + } + + when (result) { + is CompletionResult.Success -> { + state.update { + it.copy( + backupCards = it.backupCards + result.data, + backupCardsNumber = it.backupCardsNumber + 1, + ) + } + setNumberOfBackupCards(state.value.backupCardsNumber) + } + is CompletionResult.Failure -> { + when (val error = result.error) { + is TangemSdkError.BackupFailedNotEmptyWallets -> { + _uiState.update { st -> + st.copy( + dialog = resetBackupCardDialog( + onReset = { resetBackupCard(cardId = error.cardId) }, + onDismiss = { _uiState.update { it.copy(dialog = null) } }, + ), + ) + } + } + is TangemSdkError.IssuerSignatureLoadingFailed -> { + _uiState.update { st -> + st.copy( + dialog = backupCardAttestationFailedDialog( + onDismiss = { _uiState.update { it.copy(dialog = null) } }, + ), + ) + } + } + else -> FirebaseCrashlytics.getInstance().recordException(result.error) + } + } + } + } + } + + private fun resetBackupCard(cardId: String) { + modelScope.launch { + tangemSdkManager.resetToFactorySettings( + cardId = cardId, + allowsRequestAccessCodeFromRepository = false, + ) + } + } +} \ 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..9bf7b55ef5 --- /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,111 @@ +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(), + ) { + Content(state) + } + + 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.Content(state: MultiWalletBackupUM) { + Column( + Modifier + .fillMaxWidth() + .padding(start = 32.dp, end = 32.dp, bottom = 74.dp) + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + 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), + ) + } + + 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), + enabled = state.finalizeButtonEnabled, + onClick = state.onFinalizeButtonClick, + ) +} + +@Preview +@Composable +private fun PreviewWallet1() { + TangemThemePreview { + MultiWalletBackup( + state = MultiWalletBackupUM( + title = stringReference("Title"), + bodyText = stringReference("Body body body"), + ), + ) + } +} \ 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..96cc144862 --- /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,29 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference + +@Immutable +data class MultiWalletBackupUM( + val title: TextReference = stringReference(""), + val bodyText: TextReference = stringReference(""), + val addBackupButtonEnabled: Boolean = true, + val addBackupButtonLoading: Boolean = false, + val finalizeButtonEnabled: Boolean = false, + val onAddBackupClick: () -> Unit = {}, + val onFinalizeButtonClick: () -> Unit = {}, + val onSkipButtonClick: () -> Unit = {}, + val dialog: MultiWalletBackupUMDialog? = null, +) + +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/chooseoption/Wallet1ChooseOptionComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/Wallet1ChooseOptionComponent.kt new file mode 100644 index 0000000000..11b7cacc0e --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/Wallet1ChooseOptionComponent.kt @@ -0,0 +1,26 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.ui.Wallet1ChooseOption +import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState + +class Wallet1ChooseOptionComponent( + context: AppComponentContext, + private val onNextStep: (OnboardingMultiWalletState.Step) -> Unit, +) : AppComponentContext by context, MultiWalletChildComponent { + + @Composable + override fun Content(modifier: Modifier) { + Wallet1ChooseOption( + onBackupClick = { + onNextStep(OnboardingMultiWalletState.Step.AddBackupDevice) + }, + onScipClick = { + onNextStep(OnboardingMultiWalletState.Step.Done) + }, + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/ui/Wallet1ChooseOption.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/ui/Wallet1ChooseOption.kt new file mode 100644 index 0000000000..d2aa92ed93 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/ui/Wallet1ChooseOption.kt @@ -0,0 +1,67 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.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.unit.dp +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onboarding.v2.impl.R + +@Composable +fun Wallet1ChooseOption(onScipClick: () -> Unit, onBackupClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + verticalArrangement = Arrangement.Bottom, + ) { + // val pagerState = rememberPagerState(pageCount = { 10 }) // TODO [REDACTED_TASK_KEY] + + Column( + Modifier + .fillMaxWidth() + .padding(start = 32.dp, end = 32.dp, bottom = 24.dp) + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.onboarding_create_wallet_header), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 16.dp), + ) + + Text( + text = stringResource(R.string.onboarding_create_wallet_body), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 12.dp), + ) + } + + 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_button_backup_now), + onClick = onBackupClick, + ) + + SecondaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResource(R.string.onboarding_button_skip_backup), + onClick = onScipClick, + ) + } +} \ 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..0ab365e362 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/MultiWalletCreateWalletComponent.kt @@ -0,0 +1,45 @@ +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 com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState +import kotlinx.coroutines.launch + +class MultiWalletCreateWalletComponent( + context: AppComponentContext, + params: MultiWalletChildParams, + onNextStep: (OnboardingMultiWalletState.Step) -> 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(onNextStep) + } + } + + @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..88bb4abd0d --- /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,129 @@ +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.core.ui.extensions.resourceReference +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.impl.R +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.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState +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( + title = if (params.parentParams.withSeedPhraseFlow) { + resourceReference(R.string.onboarding_create_wallet_options_title) + } else { + resourceReference(R.string.onboarding_create_wallet_header) + }, + bodyText = if (params.parentParams.withSeedPhraseFlow) { + resourceReference(R.string.onboarding_create_wallet_options_message) + } else { + resourceReference(R.string.onboarding_create_wallet_body) + }, + onCreateWalletClick = { createWallet(false) }, + showOtherOptionsButton = params.parentParams.withSeedPhraseFlow, + onOtherOptionsClick = { + modelScope.launch { + onDone.emit(OnboardingMultiWalletState.Step.SeedPhrase) + } + }, + 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) + + if (params.parentParams.withSeedPhraseFlow) { + onDone.emit(OnboardingMultiWalletState.Step.AddBackupDevice) + } else { + onDone.emit(OnboardingMultiWalletState.Step.ChooseBackupOption) + } + + // 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..6c881297c3 --- /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,108 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.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.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.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.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, + ) { + Column( + Modifier + .fillMaxWidth() + .padding(start = 32.dp, end = 32.dp, bottom = 24.dp) + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + 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), + ) + } + + 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, + ) + + if (state.showOtherOptionsButton) { + 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(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + MultiWalletCreateWallet( + state = MultiWalletCreateWalletUM( + title = stringReference("Title"), + bodyText = stringReference("Body body body"), + onCreateWalletClick = {}, + showOtherOptionsButton = 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..59a5a9ace0 --- /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,23 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state + +import com.tangem.core.ui.extensions.TextReference + +data class MultiWalletCreateWalletUM( + val title: TextReference, + val bodyText: TextReference, + val showOtherOptionsButton: Boolean, + val onCreateWalletClick: () -> Unit, + 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/child/seedphrase/MultiWalletSeedPhraseComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/MultiWalletSeedPhraseComponent.kt new file mode 100644 index 0000000000..505018842e --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/MultiWalletSeedPhraseComponent.kt @@ -0,0 +1,49 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase + +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.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +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.seedphrase.model.MultiWalletSeedPhraseModel +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.MultiWalletSeedPhrase +import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch + +@Suppress("UnusedPrivateMember") +class MultiWalletSeedPhraseComponent( + context: AppComponentContext, + params: MultiWalletChildParams, + onNextStep: (OnboardingMultiWalletState.Step) -> Unit, + backButtonClickFlow: SharedFlow, +) : AppComponentContext by context, MultiWalletChildComponent { + + private val model: MultiWalletSeedPhraseModel = getOrCreateModel() + + init { + componentScope.launch { + backButtonClickFlow.collect { + model.onBack() + } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + BackHandler(enabled = true) { + model.onBack() + } + + MultiWalletSeedPhrase( + 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/seedphrase/model/MnemonicRepository.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MnemonicRepository.kt new file mode 100644 index 0000000000..6b6ae2b138 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MnemonicRepository.kt @@ -0,0 +1,35 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model + +import android.content.Context +import com.tangem.crypto.bip39.DefaultMnemonic +import com.tangem.crypto.bip39.EntropyLength +import com.tangem.crypto.bip39.Mnemonic +import com.tangem.crypto.bip39.Wordlist +import com.tangem.sdk.extensions.getWordlist +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class MnemonicRepository @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val wordlist = Wordlist.getWordlist(context) + + val words: Set = wordlist.words.toHashSet() + + fun generateMnemonic(type: MnemonicType = MnemonicType.Words12): Mnemonic = DefaultMnemonic( + entropy = when (type) { + MnemonicType.Words12 -> EntropyLength.Bits128Length + MnemonicType.Words24 -> EntropyLength.Bits256Length + }, + wordlist = wordlist, + ) + + fun generateMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic( + mnemonic = mnemonicString, + wordlist = wordlist, + ) + + enum class MnemonicType { + Words12, Words24, + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt new file mode 100644 index 0000000000..c867aaf14c --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt @@ -0,0 +1,106 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model + +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.GenerateSeedPhraseUiStateBuilder +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.SeedPhraseCheckUiStateBuilder +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@ComponentScoped +class MultiWalletSeedPhraseModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val mnemonicRepository: MnemonicRepository, + private val urlOpener: UrlOpener, +) : Model() { + + private val state = MutableStateFlow(SeedPhraseState()) + private val _uiState = MutableStateFlow(getStartState()) + + private val generateSeedPhraseUiStateBuilder = GenerateSeedPhraseUiStateBuilder( + state = state, + updateUiState = { block -> updateUiStateSpecific(block) }, + onContinue = { openWordsCheck() }, + ) + + private val seedPhraseCheckUiStateBuilder = SeedPhraseCheckUiStateBuilder( + state = state, + currentUiState = { _uiState.value }, + updateUiState = { block -> updateUiStateSpecific(block) }, + importWallet = { }, + ) + + val uiState = _uiState.asStateFlow() + + fun onBack() { + when (_uiState.value) { + is MultiWalletSeedPhraseUM.Start -> { + // TODO navigate back + } + is MultiWalletSeedPhraseUM.Import, + is MultiWalletSeedPhraseUM.GenerateSeedPhrase, + -> { + _uiState.value = getStartState() + state.value = SeedPhraseState() + } + is MultiWalletSeedPhraseUM.GeneratedWordsCheck -> { + openGeneratedSeedPhrase() + } + } + } + + private fun getStartState(): MultiWalletSeedPhraseUM { + return MultiWalletSeedPhraseUM.Start( + onImportSeedPhraseClicked = {}, + onGenerateSeedPhraseClicked = { + generateSeedPhrase() + openGeneratedSeedPhrase() + }, + onLearnMoreClicked = { + urlOpener.openUrl(seedPhraseLearnMoreUrl()) + }, + ) + } + + private fun openGeneratedSeedPhrase() { + // TODO analytics SeedPhraseEvents.ButtonGenerateSeedPhrase + + val words12 = state.value.generatedWords12 ?: return + val words24 = state.value.generatedWords24 ?: return + + // TODO disallow screenshots + + _uiState.value = generateSeedPhraseUiStateBuilder.getState( + generatedWords12 = words12, + generatedWords24 = words24, + ) + } + + private fun openWordsCheck() { + _uiState.value = seedPhraseCheckUiStateBuilder.getState() + } + + private fun generateSeedPhrase() { + val words12 = mnemonicRepository.generateMnemonic(MnemonicRepository.MnemonicType.Words12) + val words24 = mnemonicRepository.generateMnemonic(MnemonicRepository.MnemonicType.Words24) + + state.update { + it.copy( + generatedWords12 = words12, + generatedWords24 = words24, + ) + } + } + + private inline fun updateUiStateSpecific(block: (T) -> T) { + _uiState.update { + if (it !is T) return@update it + block(it) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/SeedPhraseState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/SeedPhraseState.kt new file mode 100644 index 0000000000..796cd1599a --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/SeedPhraseState.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model + +import com.tangem.crypto.bip39.Mnemonic + +data class SeedPhraseState( + val generatedWords12: Mnemonic? = null, + val generatedWords24: Mnemonic? = null, + val words24Option: Boolean = false, + val readyToImport: Boolean = false, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt new file mode 100644 index 0000000000..af12fa1b39 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt @@ -0,0 +1,27 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model + +import android.net.Uri +import com.tangem.utils.SupportedLanguages +import java.util.Locale + +internal fun seedPhraseLearnMoreUrl(): String { + val language = Locale.getDefault().language + + val languageBy = "by" + if (SupportedLanguages.RUSSIAN.equals(language, true) || + languageBy.equals(language, true) + ) { + SupportedLanguages.RUSSIAN + } else { + SupportedLanguages.ENGLISH + } + + val webUri = Uri.Builder() + .scheme("https") + .authority("tangem.com") + .appendPath(language) + .appendPath("blog/post/seed-phrase-a-risky-solution") + .build() + + return webUri.toString() +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/GenerateSeedPhraseUiStateBuilder.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/GenerateSeedPhraseUiStateBuilder.kt new file mode 100644 index 0000000000..2d45732a3a --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/GenerateSeedPhraseUiStateBuilder.kt @@ -0,0 +1,52 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder + +import com.tangem.crypto.bip39.Mnemonic +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.SeedPhraseState +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +class GenerateSeedPhraseUiStateBuilder( + private val state: MutableStateFlow, + private val updateUiState: ( + (MultiWalletSeedPhraseUM.GenerateSeedPhrase) -> MultiWalletSeedPhraseUM.GenerateSeedPhrase, + ) -> Unit, + private val onContinue: () -> Unit, +) { + + fun getState(generatedWords12: Mnemonic, generatedWords24: Mnemonic): MultiWalletSeedPhraseUM.GenerateSeedPhrase { + val words12 = generatedWords12.mnemonicComponents.mapIndexed { index, s -> + MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index + 1, s) + }.toImmutableList() + val words24 = generatedWords24.mnemonicComponents.mapIndexed { index, s -> + MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index + 1, s) + }.toImmutableList() + + return MultiWalletSeedPhraseUM.GenerateSeedPhrase( + words24OptionSelected = state.value.words24Option, + words = words12, + onWords24OptionSwitch = { switchType(words12, words24) }, + onContinueClick = onContinue, + ) + } + + private fun switchType( + generatedWords12: ImmutableList, + generatedWords24: ImmutableList, + ) { + updateUiState { uiSt -> + state.update { it.copy(words24Option = uiSt.words24OptionSelected.not()) } + + uiSt.copy( + words24OptionSelected = uiSt.words24OptionSelected.not(), + words = if (uiSt.words24OptionSelected) { + generatedWords12 + } else { + generatedWords24 + }, + ) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/SeedPhraseCheckUiStateBuilder.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/SeedPhraseCheckUiStateBuilder.kt new file mode 100644 index 0000000000..eca9aa9570 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/builder/SeedPhraseCheckUiStateBuilder.kt @@ -0,0 +1,105 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder + +import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.SeedPhraseState +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +class SeedPhraseCheckUiStateBuilder( + private val state: MutableStateFlow, + private val currentUiState: () -> MultiWalletSeedPhraseUM, + private val updateUiState: ( + (MultiWalletSeedPhraseUM.GeneratedWordsCheck) -> MultiWalletSeedPhraseUM.GeneratedWordsCheck, + ) -> Unit, + private val importWallet: () -> Unit, +) { + + @Suppress("MagicNumber") + fun getState(): MultiWalletSeedPhraseUM.GeneratedWordsCheck { + val wordFields = List(3) { index -> + val shownIndex = when (index) { + 0 -> 2 + 1 -> 7 + 2 -> 11 + else -> error("") + } + + MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField( + index = shownIndex, + word = TextFieldValue(""), + error = false, + onChange = { changeField -> + updateUiState { st -> + val newState = st.copy( + wordFields = st.wordFields.map { wordField -> + if (wordField.index == shownIndex) { + wordField.updateField(changeField, shownIndex) + } else { + wordField + } + }.toImmutableList(), + ) + + val allFieldsCorrect = newState.allFieldsCorrect() + state.update { it.copy(readyToImport = allFieldsCorrect) } + + newState.copy( + createWalletButtonEnabled = allFieldsCorrect, + ) + } + }, + ) + }.toImmutableList() + + return MultiWalletSeedPhraseUM.GeneratedWordsCheck( + wordFields = wordFields, + createWalletButtonEnabled = false, + createWalletButtonProgress = false, + onCreateWalletButtonClick = { + val currentState = currentUiState() as? MultiWalletSeedPhraseUM.GeneratedWordsCheck + ?: return@GeneratedWordsCheck + if (currentState.createWalletButtonEnabled) { + importWallet() + } + }, + ) + } + + private fun MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField.updateField( + newText: TextFieldValue, + shownIndex: Int, + ): MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField { + val correct = checkWordField(word = newText.text, shownIndex = shownIndex) + + return copy( + word = newText, + error = correct.not(), + ) + } + + private fun MultiWalletSeedPhraseUM.GeneratedWordsCheck.allFieldsCorrect(): Boolean { + return wordFields.all { + checkWordField( + word = it.word.text, + shownIndex = it.index, + ) + } + } + + private fun checkWordField(word: String, shownIndex: Int): Boolean { + val currentState = state.value + + currentState.generatedWords12 ?: return false + currentState.generatedWords24 ?: return false + + val wordList = if (currentState.words24Option) { + currentState.generatedWords24 + } else { + currentState.generatedWords12 + }.mnemonicComponents + + return wordList[shownIndex - 1] == word + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhrase.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhrase.kt new file mode 100644 index 0000000000..2d220858a2 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhrase.kt @@ -0,0 +1,37 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM + +@Composable +fun MultiWalletSeedPhrase(state: MultiWalletSeedPhraseUM, modifier: Modifier = Modifier) { + AnimatedContent( + modifier = modifier.navigationBarsPadding(), + targetState = state, + transitionSpec = { + val direction = if (initialState.order < targetState.order) { + AnimatedContentTransitionScope.SlideDirection.Start + } else { + AnimatedContentTransitionScope.SlideDirection.End + } + + slideIntoContainer(towards = direction, animationSpec = tween()) + .togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween())) + }, + contentKey = { st -> st::class.java }, + label = "animatedContent", + ) { st -> + when (st) { + is MultiWalletSeedPhraseUM.Start -> MultiWalletSeedPhraseStart(state = st) + is MultiWalletSeedPhraseUM.GenerateSeedPhrase -> MultiWalletSeedPhraseWords(state = st) + is MultiWalletSeedPhraseUM.GeneratedWordsCheck -> MultiWalletSeedPhraseWordsCheck(state = st) + is MultiWalletSeedPhraseUM.Import -> MultiWalletSeedPhraseImport(state = st) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt new file mode 100644 index 0000000000..532ac00f37 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt @@ -0,0 +1,201 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui + +import androidx.compose.animation.* +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +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.seedphrase.ui.state.MultiWalletSeedPhraseUM +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.utils.InvalidWordsColorTransformation +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun MultiWalletSeedPhraseImport(state: MultiWalletSeedPhraseUM.Import, modifier: Modifier = Modifier) { + Box(modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + Text( + text = stringResource(id = R.string.onboarding_seed_import_message), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(start = 36.dp, end = 36.dp, top = 24.dp, bottom = 16.dp) + .fillMaxWidth(), + ) + + PhraseBlock( + state = state, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + OutlineTextFieldWithIcon( + modifier = modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + value = state.passPhrase, + onValueChange = state.passPhraseChange, + iconResId = R.drawable.ic_information_24, + iconColor = TangemTheme.colors.icon.informative, + label = stringResource(id = R.string.common_passphrase), + placeholder = stringResource(id = R.string.send_optional_field), + onIconClick = state.onPassphraseInfoClick, + ) + + Box(Modifier.weight(1f)) { + PrimaryButtonIconEnd( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(16.dp) + .fillMaxWidth(), + text = stringResource(id = R.string.common_import), + iconResId = R.drawable.ic_tangem_24, + enabled = state.createWalletEnabled, + showProgress = state.createWalletProgress, + onClick = state.createWalletClick, + ) + } + } + + val keyboard by keyboardAsState() + if (keyboard is Keyboard.Opened) { + SuggestionsBlock( + modifier = Modifier + .align(Alignment.BottomCenter) + .imePadding(), + suggestionsList = state.suggestionsList, + onClick = { index -> state.onSuggestionClick(state.suggestionsList[index]) }, + ) + } + } + + PassphraseInfoBottomSheet(state.infoBottomSheetConfig) +} + +@Composable +private fun PhraseBlock(state: MultiWalletSeedPhraseUM.Import, modifier: Modifier = Modifier) { + val warningColor = TangemTheme.colors.text.warning + val invalidWordsColorTransformation = remember(state.invalidWords, warningColor) { + InvalidWordsColorTransformation( + wordsToBrush = state.invalidWords, + style = SpanStyle(color = warningColor), + ) + } + + Column( + modifier = modifier.fillMaxWidth(), + ) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size142), + value = state.passPhrase, + onValueChange = state.passPhraseChange, + textStyle = TangemTheme.typography.body1, + singleLine = false, + visualTransformation = invalidWordsColorTransformation, + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(TangemTheme.dimens.size32), + ) { + if (state.wordsErrorText != null) { + Text( + modifier = Modifier.fillMaxSize(), + text = state.wordsErrorText.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.warning, + ) + } + } + } +} + +@Composable +private fun SuggestionsBlock( + suggestionsList: ImmutableList, + onClick: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + modifier = modifier, + enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }), + exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(), + visible = suggestionsList.isNotEmpty(), + ) { + LazyRow( + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16), + ) { + items(suggestionsList.size) { index -> + SuggestionButton( + modifier = Modifier.rowPadding( + index = index, + rowSize = suggestionsList.size, + outSide = TangemTheme.dimens.size0, + inSide = TangemTheme.dimens.size4, + ), + text = suggestionsList[index], + onClick = { onClick(index) }, + ) + } + } + } +} + +@Composable +private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) { + Box(modifier = modifier.clickable(onClick = onClick)) { + Notifier( + text = text, + backgroundColor = TangemTheme.colors.icon.primary1, + textColor = TangemTheme.colors.text.primary2, + ) + } +} + +private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: Dp): Modifier = when (index) { + 0 -> this.padding(start = outSide, end = inSide) + rowSize - 1 -> this.padding(start = inSide, end = outSide) + else -> this.padding(horizontal = inSide) +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview( + alwaysShowBottomSheets = false, + ) { + MultiWalletSeedPhraseImport( + state = MultiWalletSeedPhraseUM.Import( + wordsErrorText = stringReference("Error"), + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseStart.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseStart.kt new file mode 100644 index 0000000000..aff9adb6f1 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseStart.kt @@ -0,0 +1,158 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +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.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.LineBreak +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.SecondaryButton +import com.tangem.core.ui.components.SpacerH32 +import com.tangem.core.ui.components.SpacerW8 +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.seedphrase.ui.state.MultiWalletSeedPhraseUM + +@Composable +internal fun MultiWalletSeedPhraseStart(state: MultiWalletSeedPhraseUM.Start, modifier: Modifier = Modifier) { + Column( + modifier.padding(top = 48.dp), + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .weight(1f), + verticalArrangement = Arrangement.spacedBy(28.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier.size(56.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_onboarding_text_edit_56), + contentDescription = null, + ) + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.icon.warning.copy(alpha = 0.12f), + shape = RoundedCornerShape(TangemTheme.dimens.radius8), + ), + ) { + Text( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.size12, vertical = TangemTheme.dimens.size4), + text = stringResource(id = R.string.onboarding_seed_phrase_intro_legacy), + color = TangemTheme.colors.text.warning, + style = TangemTheme.typography.body1, + ) + } + BodyContent(state) + } + SpacerH32() + Buttons(state) + } +} + +@Composable +private fun BodyContent(state: MultiWalletSeedPhraseUM.Start, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = stringResource(id = R.string.onboarding_seed_intro_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = stringResource(id = R.string.onboarding_seed_intro_message), + style = TangemTheme.typography.body1 + .copy(lineBreak = LineBreak.Paragraph), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 32.dp) + .fillMaxWidth(), + ) + ReadMoreBlock(state) + } +} + +@Composable +private fun ReadMoreBlock(state: MultiWalletSeedPhraseUM.Start, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxWidth() + .padding(top = 16.dp), + ) { + val shape = TangemTheme.shapes.roundedCornersLarge + Row( + modifier = Modifier + .align(Alignment.Center) + .clip(shape) + .border( + width = 1.dp, + color = TangemTheme.colors.stroke.primary, + shape = shape, + ) + .clickable(onClick = state.onLearnMoreClicked) + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_top_right_24), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + SpacerW8() + Text( + modifier = Modifier.align(Alignment.CenterVertically), + style = TangemTheme.typography.button, + text = stringResource(id = R.string.onboarding_seed_button_read_more), + ) + } + } +} + +@Composable +private fun Buttons(state: MultiWalletSeedPhraseUM.Start, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .imePadding(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.onboarding_seed_intro_button_generate), + onClick = state.onGenerateSeedPhraseClicked, + ) + SecondaryButton( + modifier = Modifier + .fillMaxWidth(), + text = stringResource(id = R.string.onboarding_seed_intro_button_import), + onClick = state.onImportSeedPhraseClicked, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + MultiWalletSeedPhraseStart( + state = MultiWalletSeedPhraseUM.Start(), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWords.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWords.kt new file mode 100644 index 0000000000..4e651c9944 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWords.kt @@ -0,0 +1,192 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.pluralStringResource +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.LayoutDirection +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +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.seedphrase.ui.state.MultiWalletSeedPhraseUM +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun MultiWalletSeedPhraseWords(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifier: Modifier = Modifier) { + Column(modifier.fillMaxSize()) { + SegmentSeedBlock( + modifier = Modifier.padding(vertical = 20.dp), + state = state, + ) + + TitleBlock(state) + + SeedPhraseGridBlock( + mnemonicGridItems = state.words, + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(top = 20.dp, bottom = 32.dp), + ) + + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + text = stringResource(id = R.string.common_continue), + onClick = state.onContinueClick, + ) + } +} + +@Suppress("ComplexCondition", "MagicNumber") +@Composable +private fun SegmentSeedBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifier: Modifier = Modifier) { + SegmentedButtons( + modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing76), + config = persistentListOf(0, 1), + initialSelectedItem = if (state.words24OptionSelected.not()) 0 else 1, + onClick = { + if (state.words24OptionSelected && it == 0 || state.words24OptionSelected.not() && it == 1) { + state.onWords24OptionSwitch() + } + }, + ) { + Text( + text = pluralStringResource( + id = R.plurals.onboarding_seed_generate_words_count, + count = if (it == 0) 12 else 24, + if (it == 0) 12 else 24, + ), + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing10) + .fillMaxWidth(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun TitleBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding(horizontal = TangemTheme.dimens.size36) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = stringResource(id = R.string.onboarding_seed_generate_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = pluralStringResource( + id = R.plurals.onboarding_seed_generate_message_words_count, + count = if (state.words24OptionSelected.not()) 12 else 24, + if (state.words24OptionSelected.not()) 12 else 24, + ), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun SeedPhraseGridBlock(mnemonicGridItems: ImmutableList, modifier: Modifier = Modifier) { + VerticalGrid( + modifier = modifier + .verticalScroll(rememberScrollState()), + items = mnemonicGridItems, + ) { item -> + Row( + modifier = Modifier.padding(all = TangemTheme.dimens.size8), + verticalAlignment = Alignment.CenterVertically, + ) { + if (LocalLayoutDirection.current == LayoutDirection.Ltr) { + Text( + modifier = Modifier.width(TangemTheme.dimens.size40), + text = "${item.index}.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = item.mnemonic, + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + ) + } else { + Text( + text = item.mnemonic, + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier.width(TangemTheme.dimens.size40), + text = "${item.index}.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } + } +} + +@Composable +private inline fun VerticalGrid( + items: ImmutableList, + modifier: Modifier = Modifier, + crossinline content: @Composable (T) -> Unit, +) { + val columnLength = items.size / 2 + Row( + modifier = modifier, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + repeat(2) { index -> + Column { + for (i in 0 until columnLength) { + val item = items[index * columnLength + i] + content(item) + } + } + } + } +} + +@Preview(showBackground = true, heightDp = 640) +@Composable +private fun Preview() { + TangemThemePreview { + MultiWalletSeedPhraseWords( + state = MultiWalletSeedPhraseUM.GenerateSeedPhrase( + words = List(24) { + MnemonicGridItem( + index = it + 1, + mnemonic = "word1", + ) + }.toImmutableList(), + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWordsCheck.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWordsCheck.kt new file mode 100644 index 0000000000..8f26de11b8 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWordsCheck.kt @@ -0,0 +1,122 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +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.input.TextFieldValue +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.R +import com.tangem.core.ui.components.OutlineTextField +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun MultiWalletSeedPhraseWordsCheck( + state: MultiWalletSeedPhraseUM.GeneratedWordsCheck, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + Text( + text = stringResource(R.string.onboarding_seed_user_validation_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = 48.dp, start = 36.dp, end = 36.dp, bottom = 16.dp) + .fillMaxWidth(), + ) + Text( + text = stringResource(R.string.onboarding_seed_user_validation_message), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 36.dp) + .fillMaxWidth(), + ) + + Fields( + state = state, + modifier = Modifier + .padding(vertical = 30.dp, horizontal = 16.dp), + ) + + Box( + Modifier.weight(1f), + ) { + PrimaryButtonIconEnd( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(16.dp) + .fillMaxWidth(), + text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet), + iconResId = R.drawable.ic_tangem_24, + enabled = state.createWalletButtonEnabled, + showProgress = state.createWalletButtonProgress, + onClick = state.onCreateWalletButtonClick, + ) + } + } +} + +@Composable +private fun Fields(state: MultiWalletSeedPhraseUM.GeneratedWordsCheck, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + state.wordFields.forEach { field -> + OutlineTextField( + value = field.word, + onValueChange = field.onChange, + label = field.index.toString(), + isError = field.error, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + MultiWalletSeedPhraseWordsCheck( + state = MultiWalletSeedPhraseUM.GeneratedWordsCheck( + wordFields = persistentListOf( + MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField( + index = 2, + word = TextFieldValue(text = "word"), + onChange = {}, + error = false, + ), + MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField( + index = 7, + word = TextFieldValue(text = "wor"), + onChange = {}, + error = true, + ), + MultiWalletSeedPhraseUM.GeneratedWordsCheck.WordField( + index = 11, + word = TextFieldValue(text = "word"), + onChange = {}, + error = false, + ), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/PassphraseInfoBottomSheet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/PassphraseInfoBottomSheet.kt new file mode 100644 index 0000000000..fdf30402ea --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/PassphraseInfoBottomSheet.kt @@ -0,0 +1,90 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButton +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.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R + +@Composable +fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.primary, + ) { _: TangemBottomSheetConfigContent.Empty -> + PassphraseInfoBottomSheetContent(config.onDismissRequest) + } +} + +@Composable +fun PassphraseInfoBottomSheetContent(onDismiss: () -> Unit) { + Column( + modifier = Modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxWidth(), + ) { + Icon( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(top = TangemTheme.dimens.size40) + .size(TangemTheme.dimens.size48), + painter = painterResource(id = R.drawable.ic_information_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + + Text( + text = stringResource(id = R.string.common_passphrase), + modifier = Modifier + .padding(top = TangemTheme.dimens.size40) + .align(Alignment.CenterHorizontally), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, + ) + + Text( + text = stringResource(id = R.string.onboarding_bottom_sheet_passphrase_description), + modifier = Modifier + .padding(top = TangemTheme.dimens.size16) + .padding(horizontal = TangemTheme.dimens.size24) + .align(Alignment.CenterHorizontally), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + textAlign = TextAlign.Center, + ) + + PrimaryButton( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.size16) + .padding(top = TangemTheme.dimens.size40) + .padding(bottom = TangemTheme.dimens.size32) + .fillMaxWidth(), + text = stringResource(id = R.string.common_ok), + onClick = onDismiss, + ) + } +} + +@Preview +@Composable +private fun PassphraseInfoBottomSheetContentPreview() { + TangemThemePreview { + PassphraseInfoBottomSheetContent({ }) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt new file mode 100644 index 0000000000..84e5b22628 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt @@ -0,0 +1,62 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +sealed class MultiWalletSeedPhraseUM( + val order: Int, +) { + + data class Start( + val onLearnMoreClicked: () -> Unit = {}, + val onGenerateSeedPhraseClicked: () -> Unit = {}, + val onImportSeedPhraseClicked: () -> Unit = {}, + ) : MultiWalletSeedPhraseUM(order = 0) + + data class GenerateSeedPhrase( + val words24OptionSelected: Boolean = false, + val words: ImmutableList = persistentListOf(), + val onWords24OptionSwitch: () -> Unit = {}, + val onContinueClick: () -> Unit = {}, + ) : MultiWalletSeedPhraseUM(order = 1) { + data class MnemonicGridItem( + val index: Int, + val mnemonic: String, + ) + } + + data class GeneratedWordsCheck( + val wordFields: ImmutableList = persistentListOf(), + val createWalletButtonEnabled: Boolean = false, + val createWalletButtonProgress: Boolean = false, + val onCreateWalletButtonClick: () -> Unit = {}, + ) : MultiWalletSeedPhraseUM(order = 2) { + data class WordField( + val index: Int, + val word: TextFieldValue, + val error: Boolean, + val onChange: (TextFieldValue) -> Unit, + ) + } + + data class Import( + val words: TextFieldValue = TextFieldValue(""), + val wordsChange: (TextFieldValue) -> Unit = {}, + val passPhrase: TextFieldValue = TextFieldValue(""), + val passPhraseChange: (TextFieldValue) -> Unit = {}, + val onPassphraseInfoClick: () -> Unit = {}, + val wordsErrorText: TextReference? = null, + val invalidWords: ImmutableList = persistentListOf(), + val createWalletEnabled: Boolean = false, + val createWalletProgress: Boolean = false, + val createWalletClick: () -> Unit = {}, + val suggestionsList: ImmutableList = persistentListOf(), + val onSuggestionClick: (String) -> Unit = {}, + val infoBottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, + ) : MultiWalletSeedPhraseUM(order = 1) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/utils/InvalidWordsColorTransformation.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/utils/InvalidWordsColorTransformation.kt new file mode 100644 index 0000000000..1650f7cd62 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/utils/InvalidWordsColorTransformation.kt @@ -0,0 +1,77 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.utils + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import com.tangem.utils.extensions.isSingleItem +import kotlinx.collections.immutable.ImmutableList + +class InvalidWordsColorTransformation( + private val wordsToBrush: ImmutableList, + private val style: SpanStyle, +) : VisualTransformation { + + override fun filter(text: AnnotatedString): TransformedText { + val plainText = text.text + if (wordsToBrush.isSingleItem() && wordsToBrush.first() == plainText) { + return plainText.annotate().toTransformedText() + } + + val wordList = makeSeparatedWordlist(plainText) + val annotatedString = makeAnnotatedString(wordList) + + return annotatedString.toTransformedText() + } + + private fun makeSeparatedWordlist(plainText: String): List { + val plainTextCharArray = plainText.toCharArray() + val wordlist = mutableListOf() + var wordBuilder = StringBuilder() + + for (index in plainTextCharArray.indices) { + val char = plainTextCharArray[index] + if (char.isWhitespace()) { + if (wordBuilder.isEmpty()) { + wordlist.add(WORD_SEPARATOR) + } else { + wordlist.add(wordBuilder.toString()) + wordBuilder = StringBuilder() + wordlist.add(WORD_SEPARATOR) + } + } else { + if (index == plainTextCharArray.lastIndex) { + wordBuilder.append(char) + wordlist.add(wordBuilder.toString()) + wordBuilder = StringBuilder() + } else { + wordBuilder.append(char) + } + } + } + return wordlist.toList() + } + + private fun makeAnnotatedString(wordList: List): AnnotatedString = buildAnnotatedString { + wordList.forEach { + if (it == WORD_SEPARATOR) { + append(WHITE_SPACE) + } else if (wordsToBrush.contains(it)) { + append(it.annotate()) + } else { + append(it) + } + } + } + + private fun String.annotate(): AnnotatedString = AnnotatedString(this, style) + + private fun AnnotatedString.toTransformedText(): TransformedText = TransformedText(this, OffsetMapping.Identity) + + companion object { + private const val WHITE_SPACE = " " + private const val WORD_SEPARATOR = "_$@-*" + } +} \ 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..d1df99ba69 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,9 @@ 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.child.seedphrase.model.MultiWalletSeedPhraseModel import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel import dagger.Binds import dagger.Module @@ -30,4 +33,19 @@ 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 + + @Binds + @IntoMap + @ClassKey(MultiWalletSeedPhraseModel::class) + fun provideModel4(model: MultiWalletSeedPhraseModel): 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..2cb99f6ed2 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,37 +16,33 @@ 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 + // todo check local storage for scan response return when { card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> OnboardingMultiWalletState.Step.AddBackupDevice @@ -70,71 +58,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..14260553a8 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,23 @@ 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, ) { - + /** + * Wallet1 + * CreateWallet -> ChooseBackupOption -> AddBackupDevice -> FinishBackup -> [Done] + * | + * -> [Done] + * + * Wallet2/Ring + * CreateWallet -> SeedPhrase -> AddBackupDevice -> FinishBackup -> [Done] + * | + * -> AddBackupDevice -> FinishBackup -> [Done] + */ enum class Step { - GeneratePrivateKeys, CreateWallet, AddBackupDevice, FinishBackup, Done + CreateWallet, ChooseBackupOption, SeedPhrase, 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..ec2a1c3848 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,12 @@ 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.ChooseBackupOption -> TODO() + OnboardingMultiWalletState.Step.SeedPhrase -> TODO() + 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..c698e7e721 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,86 @@ 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, + artworksState: WalletArtworksState, + isSeedPhraseState: Boolean, + modifier: Modifier = Modifier, + childContent: @Composable (Modifier) -> Unit = {}, +) { Column( - modifier = modifier - .fillMaxSize() - .navigationBarsPadding() - .verticalScroll(rememberScrollState()), + modifier = modifier.fillMaxSize(), ) { - Box(Modifier.weight(1f)) + if (isSeedPhraseState.not()) { + WalletArtworks( + url = state.artworkUrl, + modifier = Modifier + .weight(.56f) + .padding(horizontal = 34.dp) + .padding(top = 20.dp) + .fillMaxWidth(), + state = artworksState, + ) - 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, - ) + Box( + modifier = Modifier.weight(.44f), + contentAlignment = Alignment.BottomStart, + ) { + childContent(Modifier) + } + } else { + childContent(Modifier.fillMaxSize()) + } } } -@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), + artworksState = WalletArtworksState.Folded, + isSeedPhraseState = false, + childContent = { md -> + Box( + modifier = md + .fillMaxWidth() + .height(200.dp) + .background(Color.DarkGray), + ) + }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 640) +@Composable +private fun Preview2() { + TangemThemePreview { + OnboardingMultiWallet( + state = OnboardingMultiWalletUM(artworkUrl = null), + artworksState = WalletArtworksState.Folded, + isSeedPhraseState = true, + childContent = { md -> + Box( + modifier = md + .background(Color.DarkGray) + .fillMaxSize(), + ) + }, ) } } \ 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..2c1b7f784c --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/WalletArtwork.kt @@ -0,0 +1,485 @@ +@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.Canvas +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.TangemTheme +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 Hidden : 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) { + if (state is WalletArtworksState.Hidden) return + + 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)) + } + } + + val circleColor = TangemTheme.colors.background.secondary + + Canvas( + modifier = Modifier + .align(Alignment.Center) + .size(254.dp) + .fillMaxSize(), + ) { + drawCircle( + color = circleColor, + radius = size.width / 2, + center = center, + ) + } + + 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) { + WalletArtworksState.Hidden -> listOf() + 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/build.gradle.kts b/features/onramp/api/build.gradle.kts index cde16c9b92..632fd29071 100644 --- a/features/onramp/api/build.gradle.kts +++ b/features/onramp/api/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.core.ui) /* Project - Domain */ + implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) /* Compose */ 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..52be1340e5 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 @@ -2,10 +2,11 @@ package com.tangem.features.onramp.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrency interface OnrampComponent : ComposableContentComponent { - class Params + data class Params(val cryptoCurrency: CryptoCurrency) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampSuccessComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampSuccessComponent.kt new file mode 100644 index 0000000000..ceed88f9c5 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampSuccessComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onramp.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface OnrampSuccessComponent : ComposableContentComponent { + + data class Params(val txId: 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..c97691d146 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -14,13 +14,18 @@ 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) implementation(projects.core.ui) implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) /** Project - Common */ + implementation(projects.common.routing) implementation(projects.common.ui) /** Project - Domain */ @@ -30,6 +35,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) @@ -37,7 +43,6 @@ dependencies { /** DI */ implementation(deps.hilt.android) - implementation(project(":common:ui")) kapt(deps.hilt.kapt) /** AndroidX */ 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..fe1f307998 --- /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.confirmresidency.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/confirmresidency/entity/ConfirmResidencyBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyBottomSheetConfig.kt new file mode 100644 index 0000000000..baacb6b92a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyBottomSheetConfig.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.confirmresidency.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/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..d9afb5e94b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt @@ -0,0 +1,75 @@ +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.domain.onramp.OnrampSaveDefaultCountryUseCase +import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent +import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig +import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM +import com.tangem.features.onramp.impl.R +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ComponentScoped +internal class ConfirmResidencyModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val saveDefaultCountryUseCase: OnrampSaveDefaultCountryUseCase, + 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 = { + modelScope.launch { saveDefaultCountryUseCase.invoke(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/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..8e4f11228c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt @@ -0,0 +1,72 @@ +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 com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent +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, + private val selectCurrencyComponentFactory: SelectCurrencyComponent.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() }, + ), + ) + is OnrampMainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create( + context = childByContext(componentContext), + params = SelectCurrencyComponent.Params(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..de585fef2d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.onramp.main + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrency + +internal interface OnrampMainComponent : ComposableContentComponent { + + data class Params(val cryptoCurrency: CryptoCurrency, 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/AmountBlockState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt new file mode 100644 index 0000000000..d0cfbe8515 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.main.entity + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.core.ui.extensions.TextReference + +internal data class OnrampAmountBlockUM( + val currencyUM: OnrampCurrencyUM, + val amountFieldModel: AmountFieldModel, + val secondaryFieldModel: OnrampAmountSecondaryFieldUM, +) + +internal data class OnrampCurrencyUM(val code: String, val iconUrl: String, val precision: Int, val onClick: () -> Unit) + +@Immutable +internal sealed interface OnrampAmountSecondaryFieldUM { + data object Loading : OnrampAmountSecondaryFieldUM + data class Content(val amount: TextReference) : OnrampAmountSecondaryFieldUM + data class Error(val error: TextReference) : OnrampAmountSecondaryFieldUM +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt new file mode 100644 index 0000000000..0b1002a3bd --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt @@ -0,0 +1,8 @@ +package com.tangem.features.onramp.main.entity + +interface OnrampIntents { + fun onAmountValueChanged(value: String) + fun openSettings() + fun openCurrenciesList() + fun onBuyClick() +} \ 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..397816b229 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt @@ -0,0 +1,13 @@ +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 + + @Serializable + data object CurrenciesList : 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..b909ee310e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -0,0 +1,55 @@ +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, + val amountBlockState: OnrampAmountBlockUM, + ) : 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/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt new file mode 100644 index 0000000000..6c40dafb39 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -0,0 +1,85 @@ +package com.tangem.features.onramp.main.entity.factory + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.tokens.model.Amount +import com.tangem.domain.tokens.model.AmountType +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.convertToAmount +import com.tangem.features.onramp.main.entity.* +import com.tangem.features.onramp.main.entity.OnrampAmountBlockUM +import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM +import com.tangem.features.onramp.main.entity.OnrampCurrencyUM +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.utils.Provider +import java.math.BigDecimal + +internal class OnrampStateFactory( + private val currentStateProvider: Provider, + private val cryptoCurrency: CryptoCurrency, + private val onrampIntents: OnrampIntents, +) { + + fun getInitialState(currency: String, onClose: () -> Unit): OnrampMainComponentUM.InitialLoading { + return OnrampMainComponentUM.InitialLoading( + currency = currency, + onClose = onClose, + openSettings = onrampIntents::openSettings, + onBuyClick = onrampIntents::onBuyClick, + ) + } + + fun getReadyState(currency: OnrampCurrency): OnrampMainComponentUM.Content { + val state = currentStateProvider() + + val endButton = state.topBarConfig.endButtonUM.copy(enabled = true) + return OnrampMainComponentUM.Content( + topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), + buyButtonConfig = state.buyButtonConfig, + amountBlockState = getInitialAmountBlockState(currency), + ) + } + + private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampAmountBlockUM { + return OnrampAmountBlockUM( + currencyUM = OnrampCurrencyUM( + code = currency.code, + iconUrl = currency.image, + precision = currency.precision, + onClick = onrampIntents::openCurrenciesList, + ), + amountFieldModel = AmountFieldModel( + value = "", + fiatValue = "", + onValueChange = onrampIntents::onAmountValueChanged, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.None, + keyboardType = KeyboardType.Number, + ), + keyboardActions = KeyboardActions(), + isFiatValue = true, + cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrency), + fiatAmount = BigDecimal.ZERO.convertToFiatAmount(currency), + isError = false, + isWarning = false, + error = TextReference.EMPTY, + isFiatUnavailable = false, + isValuePasted = false, + onValuePastedTriggerDismiss = {}, + ), + secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, + ) + } + + private fun BigDecimal.convertToFiatAmount(currency: OnrampCurrency): Amount = Amount( + currencySymbol = currency.name, + value = this, + decimals = currency.precision, + type = AmountType.FiatType(currency.code), + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt new file mode 100644 index 0000000000..8a8c93fef6 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt @@ -0,0 +1,63 @@ +package com.tangem.features.onramp.main.entity.factory.amount + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero +import java.math.BigDecimal + +internal class OnrampAmountFieldChangeConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: String): OnrampMainComponentUM { + val state = currentStateProvider() + if (state !is OnrampMainComponentUM.Content) return state + + if (value.isEmpty()) return state.emptyState() + + val amountState = state.amountBlockState + val amountTextField = amountState.amountFieldModel + val fiatDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO + val isDoneActionEnabled = !fiatDecimal.isNullOrZero() + val amountFieldModel = amountState.amountFieldModel.copy( + fiatValue = value, + fiatAmount = amountTextField.fiatAmount.copy(value = fiatDecimal), + keyboardOptions = KeyboardOptions( + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), + ) + + return state.copy( + amountBlockState = amountState.copy( + amountFieldModel = amountFieldModel, + secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, + ), + ) + } + + private fun OnrampMainComponentUM.Content.emptyState(): OnrampMainComponentUM.Content { + val amountFieldModel = amountBlockState.amountFieldModel.copy( + value = "", + fiatValue = "", + cryptoAmount = amountBlockState.amountFieldModel.cryptoAmount.copy(value = BigDecimal.ZERO), + fiatAmount = amountBlockState.amountFieldModel.fiatAmount.copy(value = BigDecimal.ZERO), + isError = false, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.None, + keyboardType = KeyboardType.Number, + ), + ) + return copy( + amountBlockState = amountBlockState.copy( + amountFieldModel = amountFieldModel, + secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt new file mode 100644 index 0000000000..fd4c03914f --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -0,0 +1,38 @@ +package com.tangem.features.onramp.main.entity.factory.amount + +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.tokens.model.AmountType +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.utils.Provider + +internal class OnrampAmountStateFactory(private val currentStateProvider: Provider) { + + private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter( + currentStateProvider = currentStateProvider, + ) + + fun getOnAmountValueChange(value: String) = onrampAmountFieldChangeConverter.convert(value) + + fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM { + val currentState = currentStateProvider() + if (currentState !is OnrampMainComponentUM.Content) return currentState + + val amountState = currentState.amountBlockState + return currentState.copy( + amountBlockState = amountState.copy( + currencyUM = amountState.currencyUM.copy( + code = currency.code, + iconUrl = currency.image, + precision = currency.precision, + ), + amountFieldModel = amountState.amountFieldModel.copy( + fiatAmount = amountState.amountFieldModel.fiatAmount.copy( + currencySymbol = currency.code, + decimals = currency.precision, + type = AmountType.FiatType(currency.code), + ), + ), + ), + ) + } +} \ 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..50834be67d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -0,0 +1,102 @@ +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.domain.onramp.GetOnrampCurrencyUseCase +import com.tangem.domain.onramp.model.OnrampAvailability +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.features.onramp.main.OnrampMainComponent +import com.tangem.features.onramp.main.entity.OnrampIntents +import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory +import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +internal class OnrampMainComponentModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, + private val getOnrampCurrencyUseCase: GetOnrampCurrencyUseCase, + paramsContainer: ParamsContainer, +) : Model(), OnrampIntents { + + private val params: OnrampMainComponent.Params = paramsContainer.require() + private val stateFactory = OnrampStateFactory( + currentStateProvider = Provider { _state.value }, + cryptoCurrency = params.cryptoCurrency, + onrampIntents = this, + ) + private val amountStateFactory = OnrampAmountStateFactory(currentStateProvider = Provider { _state.value }) + + private val _state: MutableStateFlow = MutableStateFlow( + value = stateFactory.getInitialState( + currency = params.cryptoCurrency.name, + onClose = router::pop, + ), + ) + val state: StateFlow get() = _state.asStateFlow() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + init { + checkResidenceCountry() + } + + private fun checkResidenceCountry() { + modelScope.launch { + checkOnrampAvailabilityUseCase.invoke() + .onRight(::handleOnrampAvailability) + .onLeft { Timber.e(it) } + } + } + + private fun handleOnrampAvailability(availability: OnrampAvailability) { + when (availability) { + is OnrampAvailability.Available -> handleOnrampAvailable(availability.currency) + is OnrampAvailability.ConfirmResidency, + is OnrampAvailability.NotSupported, + -> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country)) + } + } + + private fun handleOnrampAvailable(currency: OnrampCurrency) { + _state.update { stateFactory.getReadyState(currency) } + subscribeToCurrencyUpdates() + } + + private fun subscribeToCurrencyUpdates() { + getOnrampCurrencyUseCase.invoke() + .onEach { maybeCurrency -> + val currency = maybeCurrency.getOrNull() + if (currency != null) { + _state.update { amountStateFactory.getUpdatedCurrencyState(currency) } + } + } + .launchIn(modelScope) + } + + override fun onAmountValueChanged(value: String) { + _state.update { amountStateFactory.getOnAmountValueChange(value) } + } + + override fun openSettings() { + params.openSettings() + } + + override fun onBuyClick() { + TODO("Not yet implemented") + } + + override fun openCurrenciesList() { + bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt new file mode 100644 index 0000000000..b75e7d3976 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt @@ -0,0 +1,146 @@ +package com.tangem.features.onramp.main.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection +import coil.compose.AsyncImage +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.rememberDecimalFormat +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.main.entity.OnrampAmountBlockUM +import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM +import com.tangem.features.onramp.main.entity.OnrampCurrencyUM +import kotlinx.coroutines.delay + +@Composable +internal fun OnrampAmountContent(state: OnrampAmountBlockUM, 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, + ) { + OnrampCurrencyIcon( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing24), + currencyUM = state.currencyUM, + ) + OnrampAmountField(amountField = state.amountFieldModel) + OnrampAmountSecondary(state = state.secondaryFieldModel) + } +} + +@Composable +private fun OnrampAmountField(amountField: AmountFieldModel) { + val decimalFormat = rememberDecimalFormat() + val requester = remember { FocusRequester() } + AmountTextField( + value = amountField.fiatValue, + decimals = amountField.fiatAmount.decimals, + visualTransformation = AmountVisualTransformation( + decimals = amountField.fiatAmount.decimals, + symbol = amountField.fiatAmount.currencySymbol, + decimalFormat = decimalFormat, + ), + onValueChange = amountField.onValueChange, + keyboardOptions = amountField.keyboardOptions, + keyboardActions = amountField.keyboardActions, + textStyle = TangemTheme.typography.h2.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + isEnabled = !amountField.isError, + isAutoResize = true, + isValuePasted = amountField.isValuePasted, + onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss, + modifier = Modifier + .focusRequester(requester) + .padding( + top = TangemTheme.dimens.spacing24, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ) + .requiredHeightIn(min = TangemTheme.dimens.size32), + ) + + LaunchedEffect(key1 = Unit) { + delay(timeMillis = 200) + requester.requestFocus() + } +} + +@Composable +private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + contentAlignment = Alignment.Center, + ) { + when (state) { + is OnrampAmountSecondaryFieldUM.Content -> Text( + text = state.amount.resolveReference(), + style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + is OnrampAmountSecondaryFieldUM.Error -> Text( + text = state.error.resolveReference(), + color = TangemTheme.colors.text.warning, + style = TangemTheme.typography.caption2, + textAlign = TextAlign.Center, + ) + is OnrampAmountSecondaryFieldUM.Loading -> TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(TangemTheme.dimens.size62), + ) + } + } +} + +@Composable +private fun OnrampCurrencyIcon(currencyUM: OnrampCurrencyUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier.clickable(onClick = currencyUM.onClick), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + AsyncImage( + modifier = Modifier + .size(TangemTheme.dimens.size40) + .clip(CircleShape), + model = currencyUM.iconUrl, + contentDescription = null, + ) + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } +} \ 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..14318c4511 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt @@ -0,0 +1,109 @@ +package com.tangem.features.onramp.main.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +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.imePadding(), + 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, state = state) + } + }, + 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(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .navigationBarsPadding() + .padding(bottom = TangemTheme.dimens.spacing76), + ) { + OnrampAmountContent(state = state.amountBlockState) + } +} + +@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/paymentmethod/DefaultSelectPaymentMethodComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/DefaultSelectPaymentMethodComponent.kt new file mode 100644 index 0000000000..75a8748835 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/DefaultSelectPaymentMethodComponent.kt @@ -0,0 +1,45 @@ +package com.tangem.features.onramp.paymentmethod + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodsBottomSheetConfig +import com.tangem.features.onramp.paymentmethod.ui.SelectPaymentMethodBottomSheet +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.toImmutableList + +internal class DefaultSelectPaymentMethodComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: SelectPaymentMethodComponent.Params, +) : SelectPaymentMethodComponent, AppComponentContext by context { + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val bottomSheetConfig = remember(key1 = this) { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = ::dismiss, + content = PaymentMethodsBottomSheetConfig( + selectedMethodId = params.selectedMethodId, + paymentMethods = params.paymentMethods.toImmutableList(), + ), + ) + } + SelectPaymentMethodBottomSheet(bottomSheetConfig) + } + + @AssistedFactory + interface Factory : SelectPaymentMethodComponent.Factory { + override fun create( + context: AppComponentContext, + params: SelectPaymentMethodComponent.Params, + ): DefaultSelectPaymentMethodComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/SelectPaymentMethodComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/SelectPaymentMethodComponent.kt new file mode 100644 index 0000000000..8f0090306a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/SelectPaymentMethodComponent.kt @@ -0,0 +1,16 @@ +package com.tangem.features.onramp.paymentmethod + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM + +internal interface SelectPaymentMethodComponent : ComposableBottomSheetComponent { + + data class Params( + val selectedMethodId: String, + val paymentMethods: List, + 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/paymentmethod/di/SelectPaymentMethodComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/di/SelectPaymentMethodComponentModule.kt new file mode 100644 index 0000000000..48fe4dfaa7 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/di/SelectPaymentMethodComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.paymentmethod.di + +import com.tangem.features.onramp.paymentmethod.DefaultSelectPaymentMethodComponent +import com.tangem.features.onramp.paymentmethod.SelectPaymentMethodComponent +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 SelectPaymentMethodComponentModule { + + @Binds + @Singleton + fun bindSelectPaymentMethodComponentFactory( + factory: DefaultSelectPaymentMethodComponent.Factory, + ): SelectPaymentMethodComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodUM.kt new file mode 100644 index 0000000000..08214001de --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodUM.kt @@ -0,0 +1,8 @@ +package com.tangem.features.onramp.paymentmethod.entity + +data class PaymentMethodUM( + val id: String, + val imageUrl: String, + val name: String, + val onSelect: () -> Unit, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodsBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodsBottomSheetConfig.kt new file mode 100644 index 0000000000..80aeb52757 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodsBottomSheetConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.features.onramp.paymentmethod.entity + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList + +internal data class PaymentMethodsBottomSheetConfig( + val selectedMethodId: String, + val paymentMethods: ImmutableList, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt new file mode 100644 index 0000000000..75659d6baa --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt @@ -0,0 +1,118 @@ +package com.tangem.features.onramp.paymentmethod.ui + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import coil.compose.AsyncImage +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +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.paymentmethod.entity.PaymentMethodUM +import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodsBottomSheetConfig +import kotlinx.collections.immutable.ImmutableList + +@Composable +internal fun SelectPaymentMethodBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + addBottomInsets = true, + containerColor = TangemTheme.colors.background.tertiary, + titleText = resourceReference(R.string.onramp_pay_with), + content = { contentConfig -> + SelectPaymentMethodBottomSheetContent( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing24) + .fillMaxWidth() + .wrapContentHeight(), + selectedMethodId = contentConfig.selectedMethodId, + methods = contentConfig.paymentMethods, + ) + }, + ) +} + +@Composable +private fun SelectPaymentMethodBottomSheetContent( + selectedMethodId: String, + methods: ImmutableList, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + ) { + items( + items = methods, + key = { item -> item.id }, + contentType = { item -> item::class.java }, + itemContent = { item -> + val isSelected = remember(item, selectedMethodId) { + item.id == selectedMethodId + } + val itemModifier = if (isSelected) { + Modifier + .fillMaxWidth() + .border( + width = TangemTheme.dimens.size1, + color = TangemTheme.colors.text.accent, + shape = RoundedCornerShape(TangemTheme.dimens.radius16), + ) + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .clickable(onClick = item.onSelect) + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing10, + bottom = TangemTheme.dimens.spacing10, + ) + } else { + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .clickable(onClick = item.onSelect) + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing10, + bottom = TangemTheme.dimens.spacing10, + ) + } + PaymentMethodItem( + modifier = itemModifier, + paymentMethod = item, + isSelected = isSelected, + ) + }, + ) + } +} + +@Composable +private fun PaymentMethodItem(paymentMethod: PaymentMethodUM, isSelected: Boolean, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AsyncImage( + modifier = Modifier + .size(TangemTheme.dimens.size40), + model = paymentMethod.imageUrl, + contentDescription = null, + ) + Text( + text = paymentMethod.name, + style = TangemTheme.typography.subtitle2, + color = if (isSelected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + ) + } +} \ 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..36fd583a12 --- /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( + cryptoCurrency = params.cryptoCurrency, + 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/success/DefaultOnrampSuccessComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/DefaultOnrampSuccessComponent.kt new file mode 100644 index 0000000000..d1b329d10b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/DefaultOnrampSuccessComponent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.onramp.success + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.onramp.component.OnrampSuccessComponent +import com.tangem.features.onramp.success.model.OnrampSuccessComponentModel +import com.tangem.features.onramp.success.ui.OnrampSuccessComponentContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnrampSuccessComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: OnrampSuccessComponent.Params, +) : OnrampSuccessComponent, AppComponentContext by appComponentContext { + + private val model: OnrampSuccessComponentModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsState() + + BackHandler(onBack = router::pop) + OnrampSuccessComponentContent( + state = state, + onBackClick = router::pop, + ) + } + + @AssistedFactory + interface Factory : OnrampSuccessComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnrampSuccessComponent.Params, + ): DefaultOnrampSuccessComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/di/OnrampSuccessComponentModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/di/OnrampSuccessComponentModelModule.kt new file mode 100644 index 0000000000..ba6f6664fd --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/di/OnrampSuccessComponentModelModule.kt @@ -0,0 +1,19 @@ +package com.tangem.features.onramp.success.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.success.model.OnrampSuccessComponentModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampSuccessComponentModelModule { + @Binds + @IntoMap + @ClassKey(OnrampSuccessComponentModel::class) + fun bindOnrampSuccessComponentModel(model: OnrampSuccessComponentModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/di/OnrampSuccessComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/di/OnrampSuccessComponentModule.kt new file mode 100644 index 0000000000..fe2545877b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/di/OnrampSuccessComponentModule.kt @@ -0,0 +1,19 @@ +package com.tangem.features.onramp.success.di + +import com.tangem.features.onramp.component.OnrampSuccessComponent +import com.tangem.features.onramp.success.DefaultOnrampSuccessComponent +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 OnrampSuccessComponentModule { + @Binds + @Singleton + fun bindOnrampSuccessComponentFactory( + factory: DefaultOnrampSuccessComponent.Factory, + ): OnrampSuccessComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessClickIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessClickIntents.kt new file mode 100644 index 0000000000..7c7565aef5 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessClickIntents.kt @@ -0,0 +1,5 @@ +package com.tangem.features.onramp.success.entity + +interface OnrampSuccessClickIntents { + fun goToProviderClick(providerLink: String) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt new file mode 100644 index 0000000000..71f80e2f05 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.onramp.success.entity + +import com.tangem.common.ui.expressStatus.state.ExpressStatusUM +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.extensions.TextReference + +sealed class OnrampSuccessComponentUM { + + data object Loading : OnrampSuccessComponentUM() + + data class Content( + val txId: String, + val timestamp: Long, + val currencyImageUrl: String, + val fromAmount: TextReference, + val toAmount: TextReference, + val statusBlock: ExpressStatusUM, + val providerName: TextReference, + val providerImageUrl: String, + val notification: NotificationUM?, + ) : OnrampSuccessComponentUM() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt new file mode 100644 index 0000000000..435ced42ba --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt @@ -0,0 +1,194 @@ +package com.tangem.features.onramp.success.entity.conterter + +import com.tangem.common.ui.expressStatus.state.ExpressLinkUM +import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState +import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM +import com.tangem.common.ui.expressStatus.state.ExpressStatusUM +import com.tangem.common.ui.notifications.ExpressNotificationsUM +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.onramp.model.OnrampStatus +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf +import org.joda.time.DateTime + +internal class SetOnrampSuccessContentConverter( + private val cryptoCurrency: CryptoCurrency, + private val transaction: OnrampTransaction, + private val goToProviderClick: (String) -> Unit, +) : Converter { + override fun convert(value: OnrampStatus): OnrampSuccessComponentUM { + return OnrampSuccessComponentUM.Content( + txId = value.txId, + timestamp = DateTime.parse(value.createdAt).millis, + currencyImageUrl = transaction.fromCurrency.image, + fromAmount = stringReference( + transaction.fromAmount.format { + fiat( + fiatCurrencyCode = transaction.fromCurrency.name, + fiatCurrencySymbol = transaction.fromCurrency.code, + ) + }, + ), + toAmount = stringReference( + transaction.toAmount.format { + crypto(cryptoCurrency) + }, + ), + providerName = stringReference(transaction.providerName), + providerImageUrl = transaction.providerImageUrl, + statusBlock = convertStatuses(value.status, value.externalTxUrl), + notification = getNotification(value.status, value.externalTxUrl), + ) + } + + private fun getNotification(status: OnrampStatus.Status, externalTxUrl: String?): NotificationUM? { + if (externalTxUrl == null) return null + return when (status) { + OnrampStatus.Status.Verifying -> { + ExpressNotificationsUM.NeedVerification { + goToProviderClick(externalTxUrl) + } + } + OnrampStatus.Status.Failed -> { + ExpressNotificationsUM.FailedByProvider { + goToProviderClick(externalTxUrl) + } + } + else -> null + } + } + + private fun convertStatuses(status: OnrampStatus.Status, externalTxUrl: String?): ExpressStatusUM { + val statuses = with(status) { + persistentListOf( + getAwaitingDepositItem(), + getPaymentProcessingItem(), + getBuyingItem(), + getSendingItem(), + ) + } + + return ExpressStatusUM( + title = resourceReference(R.string.common_transaction_status), + link = getStatusLink(status, externalTxUrl), + statuses = statuses, + ) + } + + private fun OnrampStatus.Status.getAwaitingDepositItem() = ExpressStatusItemUM( + text = when { + order < OnrampStatus.Status.WaitingForPayment.order -> { + resourceReference(R.string.express_exchange_status_receiving) + } + this == OnrampStatus.Status.WaitingForPayment -> { + resourceReference(R.string.express_exchange_status_receiving_active) + } + else -> { + resourceReference(R.string.express_exchange_status_received) + } + }, + state = getStatusState(OnrampStatus.Status.WaitingForPayment), + ) + + private fun OnrampStatus.Status.getPaymentProcessingItem() = ExpressStatusItemUM( + text = when { + order < OnrampStatus.Status.PaymentProcessing.order -> { + resourceReference(R.string.express_exchange_status_confirming) + } + this == OnrampStatus.Status.PaymentProcessing -> { + resourceReference(R.string.express_exchange_status_confirming_active) + } + this == OnrampStatus.Status.Verifying -> { + resourceReference(R.string.express_exchange_status_verifying) + } + this == OnrampStatus.Status.Failed -> { + resourceReference(R.string.express_exchange_status_failed) + } + else -> resourceReference(R.string.express_exchange_status_confirmed) + }, + state = when { + order < OnrampStatus.Status.PaymentProcessing.order -> { + ExpressStatusItemState.Default + } + this == OnrampStatus.Status.PaymentProcessing -> { + ExpressStatusItemState.Active + } + this == OnrampStatus.Status.Verifying -> { + ExpressStatusItemState.Warning + } + this == OnrampStatus.Status.Failed -> { + ExpressStatusItemState.Error + } + else -> { + ExpressStatusItemState.Done + } + }, + ) + + private fun OnrampStatus.Status.getBuyingItem() = ExpressStatusItemUM( + text = when { + order < OnrampStatus.Status.Paid.order -> { + resourceReference(R.string.express_status_buying, wrappedList(cryptoCurrency.name)) + } + this == OnrampStatus.Status.Paid -> { + resourceReference(R.string.express_status_buying_active, wrappedList(cryptoCurrency.name)) + } + else -> { + resourceReference(R.string.express_status_bought, wrappedList(cryptoCurrency.name)) + } + }, + state = getStatusState(OnrampStatus.Status.Paid), + ) + + private fun OnrampStatus.Status.getSendingItem() = ExpressStatusItemUM( + text = when { + order < OnrampStatus.Status.Sending.order -> { + resourceReference(R.string.express_exchange_status_sending, wrappedList(cryptoCurrency.name)) + } + this == OnrampStatus.Status.Sending -> { + resourceReference( + R.string.express_exchange_status_sending_active, + wrappedList(cryptoCurrency.name), + ) + } + else -> { + resourceReference(R.string.express_exchange_status_sent, wrappedList(cryptoCurrency.name)) + } + }, + state = getStatusState(OnrampStatus.Status.Sending), + ) + + private fun getStatusLink(status: OnrampStatus.Status, externalTxUrl: String?): ExpressLinkUM { + if (externalTxUrl == null) return ExpressLinkUM.Empty + return when (status) { + OnrampStatus.Status.Verifying, + OnrampStatus.Status.Failed, + -> { + ExpressLinkUM.Content( + icon = R.drawable.ic_arrow_top_right_24, + text = resourceReference(R.string.common_go_to_provider), + onClick = { + goToProviderClick(externalTxUrl) + }, + ) + } + else -> ExpressLinkUM.Empty + } + } + + private fun OnrampStatus.Status.getStatusState(targetState: OnrampStatus.Status) = when { + order < targetState.order -> ExpressStatusItemState.Default + this == targetState -> ExpressStatusItemState.Active + else -> ExpressStatusItemState.Done + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/previewdata/OnrampSuccessComponentUMPreviewData.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/previewdata/OnrampSuccessComponentUMPreviewData.kt new file mode 100644 index 0000000000..d7e355ba63 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/previewdata/OnrampSuccessComponentUMPreviewData.kt @@ -0,0 +1,50 @@ +package com.tangem.features.onramp.success.entity.previewdata + +import com.tangem.common.ui.expressStatus.state.ExpressLinkUM +import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState +import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM +import com.tangem.common.ui.expressStatus.state.ExpressStatusUM +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.success.entity.OnrampSuccessComponentUM +import kotlinx.collections.immutable.persistentListOf +import org.joda.time.DateTime + +internal data object OnrampSuccessComponentUMPreviewData { + + val loadingState = OnrampSuccessComponentUM.Loading + + val contentState = OnrampSuccessComponentUM.Content( + txId = "b2894851-7b63-4f56-bd75-e408f1dcba31", + timestamp = DateTime.parse("2023-09-20T13:45:10.868Z").millis, + providerName = stringReference("1Inch"), + providerImageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/1INCH1024.png", + fromAmount = stringReference("100 USDT"), + toAmount = stringReference("99.99 $"), + currencyImageUrl = "", + notification = null, + statusBlock = ExpressStatusUM( + title = resourceReference(R.string.express_exchange_status_title), + link = ExpressLinkUM.Empty, + statuses = persistentListOf( + ExpressStatusItemUM( + text = stringReference("Deposit received"), + state = ExpressStatusItemState.Done, + ), + ExpressStatusItemUM( + text = stringReference("Confirmed"), + state = ExpressStatusItemState.Done, + ), + ExpressStatusItemUM( + text = stringReference("Buying Bitcoin..."), + state = ExpressStatusItemState.Active, + ), + ExpressStatusItemUM( + text = stringReference("Sending to you"), + state = ExpressStatusItemState.Default, + ), + ), + ), + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt new file mode 100644 index 0000000000..6e925f143c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -0,0 +1,97 @@ +package com.tangem.features.onramp.success.model + +import arrow.core.getOrElse +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.onramp.GetOnrampStatusUseCase +import com.tangem.domain.onramp.GetOnrampTransactionUseCase +import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase +import com.tangem.domain.onramp.model.OnrampStatus +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.tokens.GetCryptoCurrencyUseCase +import com.tangem.features.onramp.component.OnrampSuccessComponent +import com.tangem.features.onramp.success.entity.OnrampSuccessClickIntents +import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM +import com.tangem.features.onramp.success.entity.conterter.SetOnrampSuccessContentConverter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +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 timber.log.Timber +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class OnrampSuccessComponentModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val urlOpener: UrlOpener, + private val getOnrampTransactionUseCase: GetOnrampTransactionUseCase, + private val getOnrampStatusUseCase: GetOnrampStatusUseCase, + private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, + private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, + paramsContainer: ParamsContainer, +) : Model(), OnrampSuccessClickIntents { + + private val params: OnrampSuccessComponent.Params = paramsContainer.require() + private val _state: MutableStateFlow = MutableStateFlow( + value = OnrampSuccessComponentUM.Loading, + ) + + val state: StateFlow get() = _state.asStateFlow() + + init { + loadData() + } + + override fun goToProviderClick(providerLink: String) { + urlOpener.openUrl(providerLink) + } + + private fun loadData() { + modelScope.launch { + getOnrampTransactionUseCase(txId = params.txId) + .fold( + ifLeft = { + Timber.e(it.toString()) + }, + ifRight = { transaction -> + loadTransactionStatus(transaction) + }, + ) + } + } + + private suspend fun loadTransactionStatus(transaction: OnrampTransaction) { + val cryptoCurrencies = getCryptoCurrencyUseCase( + transaction.userWalletId, + transaction.toCurrencyId, + ).getOrElse { error("Crypto currency not found") } + + getOnrampStatusUseCase(txId = params.txId) + .fold( + ifLeft = { + Timber.e(it.toString()) + }, + ifRight = { status -> + _state.update { + SetOnrampSuccessContentConverter( + cryptoCurrency = cryptoCurrencies, + transaction = transaction, + goToProviderClick = ::goToProviderClick, + ).convert(status) + } + removeTransactionIfTerminalStatus(status) + }, + ) + } + + private fun removeTransactionIfTerminalStatus(status: OnrampStatus) { + modelScope.launch { + if (status.status.isTerminal()) { + onrampRemoveTransactionUseCase(status.txId) + } + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt new file mode 100644 index 0000000000..e021c732e7 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt @@ -0,0 +1,220 @@ +package com.tangem.features.onramp.success.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.common.ui.expressStatus.ExpressStatusBlock +import com.tangem.common.ui.expressStatus.ExpressStatusNotificationBlock +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +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.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM +import com.tangem.features.onramp.success.entity.previewdata.OnrampSuccessComponentUMPreviewData + +@Composable +internal fun OnrampSuccessComponentContent( + state: OnrampSuccessComponentUM, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (state !is OnrampSuccessComponentUM.Content) { + Box(modifier.background(TangemTheme.colors.background.secondary)) + } else { + Content( + state = state, + onBackClick = onBackClick, + modifier = modifier, + ) + } +} + +@Composable +private fun Content(state: OnrampSuccessComponentUM.Content, onBackClick: () -> Unit, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier + .background(TangemTheme.colors.background.secondary) + .systemBarsPadding(), + topBar = { + TangemTopAppBar( + startButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_close_24, + onIconClicked = onBackClick, + ), + ) + }, + bottomBar = { + PrimaryButton( + text = stringResource(R.string.common_close), + onClick = onBackClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) + }, + contentWindowInsets = WindowInsetsZero, + ) { scaffoldPaddings -> + Column( + modifier = Modifier + .background(TangemTheme.colors.background.secondary) + .padding(scaffoldPaddings) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + ) { + TransactionDoneTitle( + title = resourceReference(R.string.common_in_progress), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + state.timestamp.toTimeFormat(DateTimeFormatters.dateFormatter), + state.timestamp.toTimeFormat(), + ), + ), + ) + SpacerH24() + AmountBlock(state) + SpacerH12() + FooterContainer( + footer = resourceReference(R.string.onramp_transaction_status_footer_text), + ) { + ExpressStatusBlock(state.statusBlock) + } + ExpressStatusNotificationBlock( + state.notification, + ) + } + } +} + +@Composable +private fun AmountBlock(state: OnrampSuccessComponentUM.Content) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding(16.dp), + ) { + val iconModifier = Modifier.size(size = 40.dp) + SubcomposeAsyncImage( + modifier = iconModifier, + model = ImageRequest.Builder(context = LocalContext.current) + .data(state.currencyImageUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { CircleShimmer(modifier = iconModifier) }, + error = { }, + contentDescription = null, + ) + ResizableText( + text = state.fromAmount.resolveReference(), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + ) + Text( + text = state.toAmount.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + ) + SpacerH12() + AmountProviderBlock( + state.providerName, + state.providerImageUrl, + ) + } +} + +@Composable +private fun AmountProviderBlock(providerName: TextReference, providerImageUrl: String, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = stringResource(R.string.common_with), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + SubcomposeAsyncImage( + modifier = Modifier.size(size = 16.dp), + model = ImageRequest.Builder(context = LocalContext.current) + .data(providerImageUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { CircleShimmer() }, + error = { }, + contentDescription = null, + ) + Text( + text = providerName.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun OnrampSuccessComponentContent_Preview( + @PreviewParameter(OnrampSuccessComponentContentPreviewProvider::class) + data: OnrampSuccessComponentUM, +) { + TangemThemePreview { + OnrampSuccessComponentContent( + state = data, + onBackClick = {}, + ) + } +} + +private class OnrampSuccessComponentContentPreviewProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + OnrampSuccessComponentUMPreviewData.contentState, + ) +} +// endregion \ 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/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 9975712433..21604ac0b0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -22,6 +22,7 @@ import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount @@ -93,12 +94,15 @@ internal class SendNotificationFactory( val feeError = (feeState.feeSelectorState as? FeeSelectorState.Error)?.error val recipientAddress = state.recipientState?.addressTextField?.value + val statusValue = cryptoCurrencyStatus.value as? CryptoCurrencyStatus.Loaded + val balanceAfterTransaction = statusValue?.let { it.amount - sendingAmount - feeValue } val currencyCheck = getCurrencyCheckUseCase( userWalletId = userWalletId, currencyStatus = cryptoCurrencyStatus, amount = sendingAmount, fee = feeValue, recipientAddress = recipientAddress, + balanceAfterTransaction = balanceAfterTransaction, ) buildList { addErrorNotifications( @@ -221,6 +225,11 @@ internal class SendNotificationFactory( network = cryptoCurrencyStatus.currency.network, ).leftOrNull() } + + addRentExemptionNotification( + rentWarning = currencyCheck.rentWarning, + ) + addExistentialWarningNotification( existentialDeposit = currencyCheck.existentialDeposit, feeAmount = feeState.fee?.amount?.value.orZero(), @@ -303,4 +312,9 @@ internal class SendNotificationFactory( add(NotificationUM.Warning.TooHigh(diff)) } } + + private fun MutableList.addRentExemptionNotification(rentWarning: CryptoCurrencyWarning.Rent?) { + if (rentWarning == null) return + add(NotificationUM.Solana.RentInfo(rentWarning)) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt index 2171c34904..a13f13cc19 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt @@ -68,7 +68,11 @@ internal fun checkIfFeeTooHigh(feeSelectorState: FeeSelectorState.Content, onSho val highValue = multipleFees.priority.amount.value ?: return false val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) - val diff = customValue / highValue + val diff = if (highValue > BigDecimal.ZERO) { + customValue / highValue + } else { + BigDecimal.ZERO + } val isShow = feeSelectorState.selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF if (isShow) onShow(diff.parseBigDecimal(ZERO_DECIMALS, RoundingMode.HALF_UP)) return isShow diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt index b6d93d00c8..501c61649f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt @@ -44,7 +44,7 @@ internal fun LazyListScope.notifications( is NotificationUM.Warning, -> null is NotificationUM.Error -> TangemTheme.colors.icon.warning - is NotificationUM.Info -> null + is NotificationUM.Info -> TangemTheme.colors.icon.accent }, isEnabled = !isClickDisabled, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt index 1af5a5154a..65ad8d5140 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.components.inputrow.InputRowEnterAmount import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fields.SendTextField @@ -41,7 +40,7 @@ internal fun SendCustomFee( repeat(customValues.size) { index -> val value = customValues[index] FooterContainer( - footer = value.footer.resolveReference(), + footer = value.footer, ) { if (value.label != null) { InputRowEnterInfoAmount( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 013b9e7258..ea78244e14 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -21,8 +21,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowRecipient +import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.send.impl.R @@ -32,8 +36,6 @@ import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.state.previewdata.RecipientStatePreviewData import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub -import com.tangem.core.ui.components.containers.FooterContainer -import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList @@ -107,7 +109,7 @@ private fun LazyListScope.addressItem( ) { item(key = ADDRESS_FIELD_KEY) { FooterContainer( - footer = stringResource(R.string.send_recipient_address_footer, network), + footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(network)), ) { InputRowRecipient( value = address.value, @@ -137,7 +139,7 @@ private fun LazyListScope.memoField(memoField: SendTextField.RecipientMemo?, onM value = memoField.value, label = memoField.label, placeholder = placeholder, - footer = stringResource(R.string.send_recipient_memo_footer), + footer = resourceReference(R.string.send_recipient_memo_footer), onValueChange = memoField.onValueChange, onPasteClick = onMemoChange, modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index 762dc7f7eb..cc33643eab 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -27,7 +27,7 @@ internal fun TextFieldWithPaste( onValueChange: (String) -> Unit, onPasteClick: (String) -> Unit, modifier: Modifier = Modifier, - footer: String? = null, + footer: TextReference? = null, labelStyle: TextStyle = TangemTheme.typography.body2, error: TextReference? = null, isError: Boolean = false, 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) } @@ -622,6 +638,7 @@ internal class StakingViewModel @Inject constructor( currencyStatus = cryptoCurrencyStatus, amount = amount, fee = fee, + balanceAfterTransaction = null, // ignore this in staking ) stateController.update( AddStakingNotificationsTransformer( 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 63b4b823e7..9f48919bec 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 3e33131d7d..620ca41a28 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 bb3600983b..b3360ff214 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 @@ -51,6 +51,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 @@ -86,13 +87,25 @@ 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()) + } else { + null + } private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Swap.USER_WALLET_ID_KEY) ?.unbundle(UserWalletId.serializer()) @@ -103,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 @@ -125,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 @@ -154,14 +169,15 @@ 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) { + if (fromStatus == null || wallet == null) { uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) } else { userWallet = wallet - initialCryptoCurrencyStatus = cryptoCurrencyStatus + initialFromStatus = fromStatus + initialToStatus = toStatus initTokens(isInitiallyReversed) } } @@ -182,7 +198,7 @@ internal class SwapViewModel @Inject constructor( } fun onScreenOpened() { - analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCryptoCurrency.symbol)) + analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCurrencyFrom.symbol)) } fun setRouter(router: SwapRouter) { @@ -208,14 +224,15 @@ 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, + val selectedCurrency = initialToStatus ?: swapInteractor.getInitialCurrencyToSwap( + initialCryptoCurrency = initialCurrencyFrom, state = state, isReverseFromTo = isReverseFromTo, ) + applyInitialTokenChoice( state = state, selectedCurrency = selectedCurrency, @@ -251,9 +268,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) @@ -272,15 +290,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, @@ -300,7 +318,7 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.addTokensToState( uiState = uiState, tokensDataState = tokensDataState, - fromToken = dataState.fromCryptoCurrency?.currency ?: initialCryptoCurrency, + fromToken = dataState.fromCryptoCurrency?.currency ?: initialCurrencyFrom, ) } @@ -317,7 +335,7 @@ internal class SwapViewModel @Inject constructor( uiState, fromToken.currency, toToken.currency, - initialCryptoCurrency.id.value, + initialCurrencyFrom.id.value, ) } singleTaskScheduler.scheduleTask( @@ -413,7 +431,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, ), ) @@ -451,7 +469,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, @@ -586,7 +604,7 @@ internal class SwapViewModel @Inject constructor( swapRouter.openUrl(url) } analyticsEventHandler.send( - event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol), + event = SwapEvents.ButtonExplore(initialCurrencyFrom.symbol), ) }, onStatusClick = { @@ -594,7 +612,7 @@ internal class SwapViewModel @Inject constructor( if (!txExternalUrl.isNullOrBlank()) { swapRouter.openUrl(txExternalUrl) analyticsEventHandler.send( - event = SwapEvents.ButtonStatus(initialCryptoCurrency.symbol), + event = SwapEvents.ButtonStatus(initialCurrencyFrom.symbol), ) } }, @@ -766,7 +784,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) { @@ -777,7 +795,7 @@ internal class SwapViewModel @Inject constructor( ) } } else { - fromToken = initialCryptoCurrencyStatus + fromToken = initialFromStatus toToken = foundToken.currencyStatus val newToken = toToken.currency as? CryptoCurrency.Coin @@ -1282,7 +1300,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( @@ -1291,7 +1309,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/components/ExchangeStatusNotifications.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt index 47508d351e..8d262a4334 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt @@ -19,6 +19,7 @@ internal sealed interface ExchangeStatusNotifications { sealed class CommonNotification(val config: NotificationConfig) : ExchangeStatusNotifications + @Deprecated("Use one in ExpressNotificationsUM") data class NeedVerification(val onGoToProviderClick: () -> Unit) : CommonNotification( config = NotificationConfig( title = resourceReference(R.string.express_exchange_notification_verification_title), @@ -31,6 +32,7 @@ internal sealed interface ExchangeStatusNotifications { ), ) + @Deprecated("Use one in ExpressNotificationsUM") data class Failed(val onGoToProviderClick: () -> Unit) : CommonNotification( config = NotificationConfig( title = resourceReference(R.string.express_exchange_notification_failed_title), 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/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index ec2a2b3cfb..7831a8aba3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -30,6 +30,7 @@ import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +@Deprecated("Use ExpressStatusBlock from common") @Composable internal fun ExchangeStatusBlock( statuses: ImmutableList, 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 8130b96926..b96f8a62d8 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 @@ -72,7 +72,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.* @@ -110,7 +109,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, @@ -165,7 +163,6 @@ internal class TokenDetailsViewModel @Inject constructor( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - stakingFeatureToggles = stakingFeatureToggles, getStakingIntegrationIdUseCase = getStakingIntegrationIdUseCase, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, @@ -237,9 +234,7 @@ internal class TokenDetailsViewModel @Inject constructor( subscribeOnExchangeTransactionsUpdates() updateTxHistory(refresh = false, showItemsLoading = true) - if (stakingFeatureToggles.isStakingEnabled) { - updateStakingInfo() - } + updateStakingInfo() } private fun handleBalanceHiding(owner: LifecycleOwner) { @@ -665,7 +660,7 @@ internal class TokenDetailsViewModel @Inject constructor( 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/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 7669529ac3..16ef0fd1a6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -62,10 +62,6 @@ internal interface WalletWarningsClickIntents { fun onCloseRingPromoClick() - fun onTravalaPromoClick(link: String?) - - fun onCloseTravalaPromoClick() - fun onSupportClick() fun onNoteMigrationButtonClick(url: String) @@ -89,7 +85,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, private val shouldShowRingPromoUseCase: ShouldShowRingPromoUseCase, - private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase, private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { @@ -261,34 +256,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } - override fun onTravalaPromoClick(link: String?) { - analyticsEventHandler.send( - TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( - source = AnalyticsParam.ScreensSources.Main, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala, - action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked, - ), - ) - link?.let { - viewModelScope.launch(dispatchers.main) { - router.openUrl(link) - } - } - } - - override fun onCloseTravalaPromoClick() { - analyticsEventHandler.send( - TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( - source = AnalyticsParam.ScreensSources.Main, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala, - action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed, - ), - ) - viewModelScope.launch(dispatchers.main) { - shouldShowTravalaPromoWalletUseCase.neverToShow() - } - } - override fun onSupportClick() { val scanResponse = getSelectedUserWallet()?.scanResponse ?: return val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 489719b73a..0f18d4e68c 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-870" +tangemBlockchainSdk = "develop-871" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.18-402" +tangemCardSdk = "develop-403" #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..a9830d7036 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" } @@ -411,5 +416,4 @@ private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Nexa, Blockchain.NexaTestnet, - Blockchain.Xodex, ) \ No newline at end of file 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