diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 85ed2fdfb3..8c5969e599 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.transaction) implementation(projects.domain.analytics) + implementation(projects.domain.visa) implementation(projects.common) implementation(projects.core.analytics) @@ -62,6 +63,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.datasource) implementation(projects.core.utils) + implementation(projects.core.deepLinks) implementation(projects.libs.crypto) implementation(projects.libs.auth) @@ -77,6 +79,7 @@ dependencies { implementation(projects.data.wallets) implementation(projects.data.analytics) implementation(projects.data.transaction) + implementation(projects.data.visa) /** Features */ implementation(projects.features.onboarding) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index c31c62fa66..4ef519edbd 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -23,9 +23,9 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.feature.qrscanning.QrScanningRouter import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar +import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.event.StateEvent @@ -36,6 +36,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.navigation.ManageTokensRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.tester.api.TesterRouter @@ -57,8 +58,6 @@ import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsL import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.features.intentHandler.IntentProcessor import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler -import com.tangem.tap.features.intentHandler.handlers.BuyCurrencyIntentHandler -import com.tangem.tap.features.intentHandler.handlers.SellCurrencyIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.model.Toast @@ -138,6 +137,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var qrScanningRouter: QrScanningRouter + @Inject + lateinit var deepLinksRegistry: DeepLinksRegistry + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -167,6 +169,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac checkForNotificationPermission() observeStateUpdates() + + if (intent != null) { + deepLinksRegistry.launch(intent) + } } private fun observeStateUpdates() { @@ -293,8 +299,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true } intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) intentProcessor.addHandler(WalletConnectLinkIntentHandler()) - intentProcessor.addHandler(BuyCurrencyIntentHandler()) - intentProcessor.addHandler(SellCurrencyIntentHandler()) } private fun updateAppTheme(appThemeMode: AppThemeMode) { @@ -332,6 +336,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lifecycleScope.launch { intentProcessor.handleIntent(intent) } + + if (intent != null) { + deepLinksRegistry.launch(intent) + } } override fun showSnackbar( 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 a7da950412..4f4494f2bc 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -55,6 +55,7 @@ class DialogManager : StoreSubscriber { ) is OnboardingDialog.TwinningProcessNotCompleted -> TwinningProcessNotCompletedDialog.create(context) is OnboardingDialog.InterruptOnboarding -> InterruptOnboardingDialog.create(context, state.dialog) + is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog) is WalletConnectDialog.UnsupportedCard -> SimpleAlertDialog.create( titleRes = R.string.wallet_connect_title, diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index c2bd7ec6d8..e3b376b612 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -46,6 +46,8 @@ sealed class GlobalAction : Action { data class StartForUnfinishedBackup(val addedBackupCardsCount: Int) : Onboarding() object Stop : Onboarding() + + data class ShouldResetCardOnCreate(val shouldReset: Boolean) : Onboarding() } object ScanFailsCounter { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 6556e4518b..bd03cf6cae 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -30,6 +30,11 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde is GlobalAction.Onboarding.Stop -> { globalState.copy(onboardingState = OnboardingState(false)) } + is GlobalAction.Onboarding.ShouldResetCardOnCreate -> { + globalState.copy( + onboardingState = globalState.onboardingState.copy(shouldResetOnCreate = action.shouldReset), + ) + } is GlobalAction.ScanFailsCounter.Increment -> { globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 5cdc02d930..20712881c8 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -36,4 +36,5 @@ typealias CryptoCurrencyName = String data class OnboardingState( val onboardingStarted: Boolean = false, val onboardingManager: OnboardingManager? = null, + val shouldResetOnCreate: Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 81d40b5307..9ad974cdcd 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.legacy import com.tangem.domain.redux.LegacyAction import com.tangem.tap.common.feedback.RateCanBeBetterEmail +import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.store @@ -20,6 +21,11 @@ internal object LegacyMiddleware { GlobalAction.Onboarding.Start(action.scanResponse, canSkipBackup = action.canSkipBackup), ) } + is LegacyAction.SendEmailTransactionFailed -> { + store.state.globalState.feedbackManager?.sendEmail( + SendTransactionFailedEmail(action.errorMessage), + ) + } } next(action) } 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 336c1129ed..b439d53019 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 @@ -310,4 +310,22 @@ internal object TokensDomainModule { ): GetNetworksSupportedByWallet { return GetNetworksSupportedByWallet(repository = repository) } + + @Provides + @ViewModelScoped + fun provideGetBalanceNotEnoughForFeeWarningUseCase( + currenciesRepository: CurrenciesRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetBalanceNotEnoughForFeeWarningUseCase { + return GetBalanceNotEnoughForFeeWarningUseCase(currenciesRepository, dispatchers) + } + + @Provides + @ViewModelScoped + fun provideIsAmountSubtractAvailableUseCase( + currenciesRepository: CurrenciesRepository, + dispatchers: CoroutineDispatcherProvider, + ): IsAmountSubtractAvailableUseCase { + return IsAmountSubtractAvailableUseCase(currenciesRepository, dispatchers) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 5429ab3030..5fc760156f 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -2,12 +2,13 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,24 +21,21 @@ internal object TransactionDomainModule { @Provides @ViewModelScoped - fun provideGetFeeUseCase( - walletManagersFacade: WalletManagersFacade, - dispatchers: CoroutineDispatcherProvider, - ): GetFeeUseCase { - return GetFeeUseCase(walletManagersFacade, dispatchers) + fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase { + return GetFeeUseCase(walletManagersFacade) } @Provides @ViewModelScoped fun provideSendTransactionUseCase( isDemoCardUseCase: IsDemoCardUseCase, - walletManagersFacade: WalletManagersFacade, cardSdkConfigRepository: CardSdkConfigRepository, + transactionRepository: TransactionRepository, ): SendTransactionUseCase { return SendTransactionUseCase( isDemoCardUseCase = isDemoCardUseCase, cardSdkConfigRepository = cardSdkConfigRepository, - walletManagersFacade = walletManagersFacade, + transactionRepository = transactionRepository, ) } @@ -46,4 +44,10 @@ internal object TransactionDomainModule { fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase { return CreateTransactionUseCase(transactionRepository) } + + @Provides + @ViewModelScoped + fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase { + return IsFeeApproximateUseCase(feeRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/VisaDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/VisaDomainModule.kt new file mode 100644 index 0000000000..61170463f0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/VisaDomainModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.visa.GetVisaCurrencyUseCase +import com.tangem.domain.visa.repository.VisaRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +internal object VisaDomainModule { + + @Provides + fun provideVisaCurrencyUseCase(visaRepository: VisaRepository): GetVisaCurrencyUseCase { + return GetVisaCurrencyUseCase(visaRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 33ea89804c..d9e620497a 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -85,11 +85,15 @@ class TangemSdkManager( ).also { sendScanResultsToAnalytics(it) } } - suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult { + suspend fun createProductWallet( + scanResponse: ScanResponse, + shouldReset: Boolean = false, + ): CompletionResult { return runTaskAsync( runnable = CreateProductWalletTask( cardTypesResolver = scanResponse.cardTypesResolver, derivationStyleProvider = scanResponse.derivationStyleProvider, + shouldReset = shouldReset, ), cardId = scanResponse.card.cardId, initialMessage = Message(resources.getString(R.string.initial_message_create_wallet_body)), @@ -100,6 +104,7 @@ class TangemSdkManager( suspend fun importWallet( scanResponse: ScanResponse, mnemonic: String, + shouldReset: Boolean, ): CompletionResult { val defaultMnemonic = try { DefaultMnemonic(mnemonic, tangemSdk.wordlist) @@ -108,9 +113,10 @@ class TangemSdkManager( } return runTaskAsync( CreateProductWalletTask( - scanResponse.cardTypesResolver, + cardTypesResolver = scanResponse.cardTypesResolver, derivationStyleProvider = scanResponse.derivationStyleProvider, - defaultMnemonic, + mnemonic = defaultMnemonic, + shouldReset = shouldReset, ), scanResponse.card.cardId, Message(resources.getString(R.string.initial_message_create_wallet_body)), diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletAndRescanTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletAndRescanTask.kt deleted file mode 100644 index 81a967c20b..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletAndRescanTask.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.tap.domain.tasks - -import com.tangem.common.CompletionResult -import com.tangem.common.card.Card -import com.tangem.common.card.FirmwareVersion -import com.tangem.common.core.CardSession -import com.tangem.common.core.CardSessionRunnable -import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.guard -import com.tangem.operations.PreflightReadMode -import com.tangem.operations.PreflightReadTask -import com.tangem.operations.wallet.CreateWalletTask - -@Deprecated("Use CreateProductWalletAndRescanTask instead") -class CreateWalletAndRescanTask : CardSessionRunnable { - - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { - val card = session.environment.card.guard { - callback(CompletionResult.Failure(TangemSdkError.CardError())) - return - } - val firmwareVersion = card.firmwareVersion - - val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) { - CreateWalletTask(card.supportedCurves.first()) - } else { - CreateWalletsTask() - } - - task.run(session) { result -> - when (result) { - is CompletionResult.Success -> - PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback) - is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletsTask.kt deleted file mode 100644 index 260736b303..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletsTask.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.tap.domain.tasks - -import com.tangem.common.CompletionResult -import com.tangem.common.card.Card -import com.tangem.common.card.EllipticCurve -import com.tangem.common.core.CardSession -import com.tangem.common.core.CardSessionRunnable -import com.tangem.operations.PreflightReadMode -import com.tangem.operations.PreflightReadTask -import com.tangem.operations.wallet.CreateWalletTask - -@Deprecated("Use CreateProductWalletTask instead") -class CreateWalletsTask(curves: List? = null) : CardSessionRunnable { - - private val curves = curves ?: listOf( - EllipticCurve.Secp256k1, - EllipticCurve.Ed25519, - EllipticCurve.Secp256r1, - ) - - private var index = 0 - - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { - val curve = curves[index] - createWallet(curve, session, callback) - } - - private fun createWallet( - curve: EllipticCurve, - session: CardSession, - callback: (result: CompletionResult) -> Unit, - ) { - CreateWalletTask(curve).run(session) { result -> - when (result) { - is CompletionResult.Success -> { - if (index == curves.lastIndex) { - PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback) - return@run - } - index += 1 - createWallet(curves[index], session, callback) - } - is CompletionResult.Failure -> { - callback(CompletionResult.Failure(result.error)) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CardInitializationValidator.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CardInitializationValidator.kt new file mode 100644 index 0000000000..624e1d3abe --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CardInitializationValidator.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.domain.tasks.product + +import com.tangem.common.card.CardWallet +import com.tangem.common.card.EllipticCurve + +class CardInitializationValidator(private val expectedCurves: List) { + + fun validateWallets(wallets: List): Boolean { + val curves = wallets.map { it.curve }.toSet() + return curves.size == expectedCurves.size && + curves.containsAll(expectedCurves) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index 48e5ed9a7a..f5c1a21f38 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.card.EllipticCurve +import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemSdkError @@ -24,6 +25,7 @@ import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingCommand import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.operations.read.ReadWalletsListCommand import com.tangem.operations.wallet.CreateWalletTask import com.tangem.tap.features.demo.DemoHelper import com.tangem.operations.wallet.CreateWalletResponse as SdkCreateWalletResponse @@ -60,6 +62,7 @@ class CreateProductWalletTask( private val cardTypesResolver: CardTypesResolver, private val derivationStyleProvider: DerivationStyleProvider, private val mnemonic: Mnemonic? = null, + private val shouldReset: Boolean, ) : CardSessionRunnable { override val allowsRequestAccessCodeFromRepository: Boolean = false @@ -79,7 +82,7 @@ class CreateProductWalletTask( cardTypesResolver.isTangemTwins() -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet") - else -> CreateWalletTangemWallet(mnemonic, derivationStyleProvider) + else -> CreateWalletTangemWallet(mnemonic, shouldReset, derivationStyleProvider, cardDto) } commandProcessor.proceed(cardDto, session) { when (it) { @@ -139,38 +142,38 @@ private class CreateWalletTangemNote(private val cardTypesResolver: CardTypesRes */ private class CreateWalletTangemWallet( private val mnemonic: Mnemonic?, + private val shouldReset: Boolean, private val derivationStyleProvider: DerivationStyleProvider, + cardDTO: CardDTO, ) : ProductCommandProcessor { private var primaryCard: PrimaryCard? = null + private val cardConfig = CardConfig.createConfig(cardDTO) override fun proceed( card: CardDTO, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { - val config = CardConfig.createConfig(card) val walletsOnCard = card.wallets.map { it.curve }.toSet() - val curves = config.mandatoryCurves.toSet() - .intersect(card.supportedCurves.toSet()) - .subtract(walletsOnCard).toList() - - if (curves.isEmpty()) { - val createWalletResponses = card.wallets.map { wallet -> - CreateWalletResponse(card.cardId, wallet) - } - proceedWithCreatedWallets(card, createWalletResponses, session, callback) - return + if (walletsOnCard.isEmpty()) { + createMultiWallet(card, session, callback) + } else if (shouldReset) { + resetCard(card, session, callback) + } else { + callback(CompletionResult.Failure(TangemSdkError.WalletAlreadyCreated())) } - CreateWalletsTask(curves, mnemonic).run(session) { result -> + } + + private fun createMultiWallet( + card: CardDTO, + session: CardSession, + callback: (result: CompletionResult) -> Unit, + ) { + CreateWalletsTask(cardConfig.mandatoryCurves, mnemonic).run(session) { result -> when (result) { is CompletionResult.Success -> { - proceedWithCreatedWallets( - card = card, - createWalletResponses = result.data.createWalletResponses.map { CreateWalletResponse(it) }, - session = session, - callback = callback, - ) + checkIfAllWalletsCreated(card, session, result.data, callback) } is CompletionResult.Failure -> { callback(CompletionResult.Failure(result.error)) @@ -179,6 +182,61 @@ private class CreateWalletTangemWallet( } } + private fun checkIfAllWalletsCreated( + card: CardDTO, + session: CardSession, + createResponse: CreateWalletsResponse, + callback: (result: CompletionResult) -> Unit, + ) { + if (card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) { + proceedWithCreatedWallets( + card = card, + createWalletResponses = createResponse.createWalletResponses.map { CreateWalletResponse(it) }, + session = session, + callback = callback, + ) + return + } + + val command = ReadWalletsListCommand() + command.run(session) { response -> + when (response) { + is CompletionResult.Success -> { + val cardInitializationValidator = CardInitializationValidator(cardConfig.mandatoryCurves) + if (cardInitializationValidator.validateWallets(response.data.wallets)) { + proceedWithCreatedWallets( + card = card, + createWalletResponses = createResponse.createWalletResponses.map { + CreateWalletResponse(it) + }, + session = session, + callback = callback, + ) + } else { + callback(CompletionResult.Failure(TangemSdkError.WalletAlreadyCreated())) + } + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error)) + } + } + } + + private fun resetCard( + card: CardDTO, + session: CardSession, + callback: (result: CompletionResult) -> Unit, + ) { + val resetCommand = ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository = false) + resetCommand.run(session) { + when (it) { + is CompletionResult.Success -> { + createMultiWallet(card, session, callback) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error)) + } + } + } + private fun proceedWithCreatedWallets( card: CardDTO, createWalletResponses: List, diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt index 965525c83e..16fba8e3f2 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt @@ -313,7 +313,12 @@ internal sealed class AddCustomTokenWarning(val description: TextReference) { /** * Floating button of add custom token screen * - * @property isEnabled button availability - * @property onClick lambda be invoked when button is been pressed + * @property isEnabled button availability + * @property showProgress whether circle progress indication is enabled + * @property onClick lambda be invoked when button is been pressed */ -internal data class AddCustomTokenFloatingButton(val isEnabled: Boolean, val onClick: () -> Unit) \ No newline at end of file +internal data class AddCustomTokenFloatingButton( + val isEnabled: Boolean, + val showProgress: Boolean, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt index b5274e6d12..ba7d22727c 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt @@ -91,7 +91,11 @@ internal object AddCustomTokenPreviewData { ), form = createDefaultForm(), warnings = createWarnings(), - floatingButton = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}), + floatingButton = AddCustomTokenFloatingButton( + isEnabled = false, + showProgress = false, + onClick = {}, + ), testBlock = AddCustomTokenTestBlock( chooseTokenButtonText = "Choose token", clearButtonText = "Clear address", @@ -112,7 +116,11 @@ internal object AddCustomTokenPreviewData { ), form = createDefaultForm(), warnings = createWarnings(), - floatingButton = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}), + floatingButton = AddCustomTokenFloatingButton( + isEnabled = false, + showProgress = false, + onClick = {}, + ), ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt index 558534f369..a55ee2f29e 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt @@ -32,6 +32,7 @@ internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, m text = stringResource(id = R.string.custom_token_add_token), iconResId = R.drawable.ic_plus_24, enabled = model.isEnabled, + showProgress = model.showProgress, onClick = model.onClick, ) } @@ -48,7 +49,7 @@ private fun Preview_AddCustomTokenFloatingButton( private class AddCustomTokenFloatingButtonProvider : CollectionPreviewParameterProvider( listOf( - AddCustomTokenFloatingButton(isEnabled = true, onClick = {}), - AddCustomTokenFloatingButton(isEnabled = false, onClick = {}), + AddCustomTokenFloatingButton(isEnabled = true, showProgress = false, onClick = {}), + AddCustomTokenFloatingButton(isEnabled = false, showProgress = false, onClick = {}), ), ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index aedff94b31..fae60b48c3 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -142,7 +142,11 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun createFloatingButton(): AddCustomTokenFloatingButton { - return AddCustomTokenFloatingButton(isEnabled = false, onClick = actionsHandler::onAddCustomTokenClick) + return AddCustomTokenFloatingButton( + isEnabled = true, + showProgress = false, + onClick = actionsHandler::onAddCustomTokenClick, + ) } private inner class FormStateBuilder { @@ -852,9 +856,14 @@ internal class AddCustomTokenViewModel @Inject constructor( analyticsSender.sendWhenAddTokenButtonClicked(currency) viewModelScope.launch(dispatchers.io) { + val oldButtonState = uiState.floatingButton + uiState = uiState.copySealed( + floatingButton = uiState.floatingButton.copy(isEnabled = false, showProgress = true), + ) runCatching { featureInteractor.saveToken(currency) } .onSuccess { featureRouter.openWalletScreen() } .onFailure { + uiState = uiState.copySealed(floatingButton = oldButtonState) Timber.e(it) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index e4198a521c..dfbc486b5c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -63,7 +63,8 @@ internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier checked = item.isChecked, enabled = item.isEnabled, onCheckedChange = item.onCheckedChange, - checkedColor = TangemTheme.colors.icon.accent, + checkedColor = TangemTheme.colors.control.checked, + uncheckedColor = TangemTheme.colors.icon.inactive, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt index c0ffda2914..5414ff05c6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -27,9 +27,9 @@ internal class ResetCardFragment : ComposeFragment(), StoreSubscriber Uni modifier = modifier, content = { when (state) { - is ResetCardScreenState.ResetCardScreenContent -> { - ResetCardView(state = state) - } + is ResetCardScreenState.ResetCardScreenContent -> ResetCardView(state = state) ResetCardScreenState.InitialState -> { // do nothing for now, just white screen } @@ -43,83 +37,86 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni ) } -@Suppress("LongMethod", "MagicNumber") @Composable private fun ResetCardView(state: ResetCardScreenState.ResetCardScreenContent) { + val scrollState = rememberScrollState() + Column( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.SpaceBetween, + .verticalScroll(scrollState) // set scrollState after fillMaxSize + .padding(horizontal = TangemTheme.dimens.spacing20), ) { - ScreenTitle(titleRes = R.string.card_settings_reset_card_to_factory) - Box( - modifier = Modifier - .weight(1f) - .padding(horizontal = 21.dp), - contentAlignment = Alignment.CenterStart, - ) { - Icon( - painter = painterResource(id = R.drawable.img_alert), - contentDescription = "", - tint = Color.Unspecified, - ) - } - Column( - modifier = Modifier.offset(y = (-32).dp), - verticalArrangement = Arrangement.Bottom, - ) { - Text( - text = stringResource(id = R.string.common_attention), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) + Title() + SpacerH24() + AlertImage() + SpacerH24() + Subtitle() + SpacerH16() + Description(text = state.descriptionText) + SpacerH12() + Conditions(state) + DynamicSpacer(scrollState = scrollState) + SpacerH16() + ResetButton(enabled = state.resetButtonEnabled, onResetButtonClick = state.onResetButtonClick) + SpacerH16() + } +} - Spacer(modifier = Modifier.size(24.dp)) +@Composable +private fun Title() { + Text( + text = stringResource(id = R.string.card_settings_reset_card_to_factory), + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) +} - Text( - text = state.descriptionText.resolveReference(), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - ) +@Composable +private fun AlertImage() { + Image( + painter = painterResource(id = R.drawable.img_alert_80), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size80), + ) +} - Spacer(modifier = Modifier.size(28.dp)) +@Composable +private fun Subtitle() { + Text( + text = stringResource(id = R.string.common_attention), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) +} - state.warningsToShow.forEach { - when (it) { - ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> { - ConditionCheckBox( - checkedState = state.acceptCondition1Checked, - onCheckedChange = state.onAcceptCondition1ToggleClick, - description = TextReference.Res(R.string.reset_card_to_factory_condition_1), - ) - } - ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> { - ConditionCheckBox( - checkedState = state.acceptCondition2Checked, - onCheckedChange = state.onAcceptCondition2ToggleClick, - description = TextReference.Res(R.string.reset_card_to_factory_condition_2), - ) - } - } +@Composable +private fun Description(text: TextReference) { + Text( + text = text.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + ) +} + +@Composable +private fun Conditions(state: ResetCardScreenState.ResetCardScreenContent) { + state.warningsToShow.forEach { + when (it) { + ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> { + ConditionCheckBox( + checkedState = state.acceptCondition1Checked, + onCheckedChange = state.onAcceptCondition1ToggleClick, + description = TextReference.Res(R.string.reset_card_to_factory_condition_1), + ) } - Spacer(modifier = Modifier.size(16.dp)) - Box( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 32.dp), - ) { - AnimatedContent( - targetState = state.resetButtonEnabled, - label = "Update checked state", - ) { buttonEnabled -> - DetailsMainButton( - title = stringResource(id = R.string.reset_card_to_factory_button_title), - onClick = state.onResetButtonClick, - enabled = buttonEnabled, - ) - } + ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> { + ConditionCheckBox( + checkedState = state.acceptCondition2Checked, + onCheckedChange = state.onAcceptCondition2ToggleClick, + description = TextReference.Res(R.string.reset_card_to_factory_condition_2), + ) } } } @@ -130,16 +127,11 @@ private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) Row( modifier = Modifier .fillMaxWidth() - .clickable( - onClick = { onCheckedChange.invoke(!checkedState) }, - ) - .padding(top = TangemTheme.dimens.size16, bottom = TangemTheme.dimens.size16), + .clickable(onClick = { onCheckedChange.invoke(!checkedState) }) + .padding(vertical = TangemTheme.dimens.size16), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), ) { - IconToggleButton( - checked = checkedState, - onCheckedChange = onCheckedChange, - modifier = Modifier.padding(start = TangemTheme.dimens.size20, end = TangemTheme.dimens.size20), - ) { + IconToggleButton(checked = checkedState, onCheckedChange = onCheckedChange) { AnimatedContent(targetState = checkedState, label = "Update checked state") { checked -> Icon( painter = painterResource( @@ -151,22 +143,44 @@ private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) ), contentDescription = null, tint = if (checked) { - TangemTheme.colors.icon.accent + TangemTheme.colors.control.checked } else { TangemTheme.colors.icon.secondary }, ) } } + Text( text = description.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, - modifier = Modifier.padding(end = TangemTheme.dimens.size20), ) } } +/** + * It's helps to create an adaptive layout. + * ResetButton will be attach to the bottom of large screen or will be inside scroll layout of small screen. + * + * @param scrollState flag determines if screen is small (has scroll) or large (hasn't scroll) + */ +@Composable +private fun ColumnScope.DynamicSpacer(scrollState: ScrollState) { + if (!scrollState.canScrollBackward && !scrollState.canScrollForward) { + Spacer(modifier = Modifier.weight(1f)) + } +} + +@Composable +private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) { + DetailsMainButton( + title = stringResource(id = R.string.reset_card_to_factory_button_title), + onClick = onResetButtonClick, + enabled = enabled, + ) +} + // region Preview @Composable private fun ResetCardScreenSample(modifier: Modifier = Modifier) { diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt deleted file mode 100644 index 31a7657c79..0000000000 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.tap.features.intentHandler.handlers - -import android.content.Intent -import com.tangem.tap.features.intentHandler.IntentHandler - -/** -[REDACTED_AUTHOR] - */ -class BuyCurrencyIntentHandler : IntentHandler { - - override fun handleIntent(intent: Intent?): Boolean { - // FIXME: [REDACTED_JIRA] - // val data = intent?.data ?: return false - // val currency = store.state.walletState.selectedCurrency ?: return false - // - // val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL) - // return if (data.host == successUri.host && data.authority == successUri.authority) { - // val currencyType = AnalyticsParam.CurrencyType.Currency(currency) - // Analytics.send(TokenScreenAnalyticsEvent.Bought(currencyType.value)) - // true - // } else { - // false - // } - - return false - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt deleted file mode 100644 index 52e8e12a33..0000000000 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.tap.features.intentHandler.handlers - -import android.content.Intent -import com.tangem.tap.features.intentHandler.IntentHandler - -/** -[REDACTED_AUTHOR] - */ -class SellCurrencyIntentHandler : IntentHandler { - - override fun handleIntent(intent: Intent?): Boolean { - // FIXME: [REDACTED_JIRA] - // return try { - // val intentData = intent?.data ?: return false - // val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false - // val currency = intentData.getQueryParameter(CURRENCY_CODE_PARAM) ?: return false - // val amount = intentData.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return false - // val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false - // - // Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") - // store.dispatchOnMain( - // TradeCryptoAction.SendCrypto( - // currencyId = currency, - // amount = amount, - // destinationAddress = destinationAddress, - // transactionId = transactionID, - // ), - // ) - // true - // } catch (exception: Exception) { - // Timber.d("Not MoonPay URL") - // false - // } - - return false - } - - // private companion object { - // private const val TRANSACTION_ID_PARAM = "transactionId" - // private const val CURRENCY_CODE_PARAM = "baseCurrencyCode" - // private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount" - // private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress" - // } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt index 901014d44b..0134ddaf15 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt @@ -9,4 +9,5 @@ import com.tangem.core.navigation.StateDialog sealed class OnboardingDialog : StateDialog { object TwinningProcessNotCompleted : OnboardingDialog() data class InterruptOnboarding(val onOk: VoidCallback) : OnboardingDialog() + data class WalletActivationError(val onConfirm: () -> Unit) : OnboardingDialog() } \ No newline at end of file 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 bae2e62550..418ca44742 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 @@ -120,7 +120,10 @@ private fun handleWalletAction(action: Action) { is OnboardingWalletAction.CreateWallet -> { scanResponse ?: return scope.launch { - val result = tangemSdkManager.createProductWallet(scanResponse) + val result = tangemSdkManager.createProductWallet( + scanResponse, + globalState.onboardingState.shouldResetOnCreate, + ) store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(true, result)) } } @@ -139,10 +142,15 @@ private fun handleWalletAction(action: Action) { ) onboardingManager.scanResponse = updatedResponse + store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false)) startCardActivation(updatedResponse) store.dispatch(OnboardingWalletAction.ResumeBackup) } - is CompletionResult.Failure -> Unit + is CompletionResult.Failure -> { + if (result.error is TangemSdkError.WalletAlreadyCreated) { + handleActivationError() + } + } } } is OnboardingWalletAction.FinishOnboarding -> { @@ -218,9 +226,15 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { is OnboardingWallet2Action.CreateWallet -> { scanResponse ?: return scope.launch { - val mediateResult = when (val result = tangemSdkManager.createProductWallet(scanResponse)) { + val mediateResult = when ( + val result = tangemSdkManager.createProductWallet( + scanResponse, + globalState.onboardingState.shouldResetOnCreate, + ) + ) { is CompletionResult.Success -> { Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) + store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false)) val response = CreateWalletResponse( card = result.data.card, derivedKeys = result.data.derivedKeys, @@ -230,6 +244,9 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { } is CompletionResult.Failure -> { + if (result.error is TangemSdkError.WalletAlreadyCreated) { + handleActivationError() + } CompletionResult.Failure(result.error) } } @@ -246,6 +263,7 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { val result = tangemSdkManager.importWallet( scanResponse = scanResponse, mnemonic = action.mnemonicComponents.joinToString(" "), + shouldReset = globalState.onboardingState.shouldResetOnCreate, ) ) { is CompletionResult.Success -> { @@ -259,6 +277,7 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { seedPhraseLength = action.mnemonicComponents.size, ), ) + store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false)) val response = CreateWalletResponse( card = result.data.card, derivedKeys = result.data.derivedKeys, @@ -268,6 +287,9 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { } is CompletionResult.Failure -> { + if (result.error is TangemSdkError.WalletAlreadyCreated) { + handleActivationError() + } CompletionResult.Failure(result.error) } } @@ -300,6 +322,16 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { } } +private fun handleActivationError() { + store.dispatchDialogShow( + OnboardingDialog.WalletActivationError( + onConfirm = { + store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(true)) + }, + ), + ) +} + private fun updateScanResponseAfterBackup(scanResponse: ScanResponse, backupState: BackupState): ScanResponse { val card = if (backupState.backupCardsNumber > 0) { val cardsCount = backupState.backupCardsNumber diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt index 11f7664aa8..e92ebc5ca3 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.onboarding.products.wallet.ui import androidx.compose.runtime.collectAsState -import com.tangem.feature.onboarding.api.OnboardingSeedPhrase +import com.tangem.feature.onboarding.api.OnboardingSeedPhraseScreen import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel @@ -15,7 +15,7 @@ import com.tangem.wallet.R [REDACTED_AUTHOR] */ internal class OnboardingSeedPhraseStateHandler( - private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhrase(), + private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhraseScreen(), ) { fun newState( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt new file mode 100644 index 0000000000..bc213e1d41 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs + +import android.app.Dialog +import android.content.Context +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.tangem.tap.common.extensions.dispatchDialogHide +import com.tangem.tap.common.feedback.SupportInfo +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.onboarding.OnboardingDialog +import com.tangem.tap.store +import com.tangem.wallet.R + +object WalletActivationErrorDialog { + + fun create(context: Context, dialog: OnboardingDialog.WalletActivationError): Dialog { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { + setTitle(context.getString(R.string.onboarding_activation_error_title)) + setMessage(context.getString(R.string.onboarding_activation_error_message)) + setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() } + setNegativeButton(R.string.chat_button_title) { _, _ -> + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + } + setOnDismissListener { store.dispatchDialogHide() } + setCancelable(false) + }.create() + } +} \ No newline at end of file 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 b1db79189b..36123cf1a9 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 @@ -22,6 +22,7 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.send.redux.PrepareSendScreen +import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens import com.tangem.tap.proxy.redux.DaggerGraphState @@ -50,19 +51,18 @@ object TradeCryptoMiddleware { if (DemoHelper.tryHandle(state, action)) return when (action) { - is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen() is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) - is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action) - is TradeCryptoAction.New.Sell -> proceedNewSellAction(action) - is TradeCryptoAction.New.Swap -> openSwap( + is TradeCryptoAction.Buy -> proceedBuyAction(state, action) + is TradeCryptoAction.Sell -> proceedSellAction(action) + is TradeCryptoAction.Swap -> openSwap( currency = action.cryptoCurrency, ) - is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action) - is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action) + is TradeCryptoAction.SendToken -> handleSendToken(action = action) + is TradeCryptoAction.SendCoin -> handleSendCoin(action = action) } } - private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) { + private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { val networkAddress = action.cryptoCurrencyStatus.value.networkAddress ?.defaultAddress ?.let(NetworkAddress.Address::value) @@ -119,7 +119,7 @@ object TradeCryptoMiddleware { } } - private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) { + private fun proceedSellAction(action: TradeCryptoAction.Sell) { val networkAddress = action.cryptoCurrencyStatus.value.networkAddress ?.defaultAddress ?.let(NetworkAddress.Address::value) @@ -139,33 +139,6 @@ object TradeCryptoMiddleware { } } - private fun preconfigureAndOpenSendScreen() = scope.launch { - // FIXME: [REDACTED_JIRA] - // val selectedWalletData = store.state.walletState.selectedWalletData ?: return - // - // Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency))) - // val walletManager = store.state.walletState.getWalletManager(selectedWalletData.currency).guard { - // FirebaseCrashlytics.getInstance().recordException(IllegalStateException("WalletManager is null")) - // return - // } - // - // store.dispatchOnMain( - // PrepareSendScreen( - // walletManager = walletManager, - // coinAmount = walletManager.wallet.amounts[AmountType.Coin], - // coinRate = selectedWalletData.fiatRate, - // ), - // ) - // store.dispatchOnMain( - // SendAction.SendSpecificTransaction( - // sendAmount = action.amount, - // destinationAddress = action.destinationAddress, - // transactionId = action.transactionId, - // ), - // ) - // store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send)) - } - private fun openReceiptUrl(transactionId: String) { store.dispatchOnMain(NavigationAction.PopBackTo()) store.state.globalState.exchangeManager.getSellCryptoReceiptUrl( @@ -182,7 +155,7 @@ object TradeCryptoMiddleware { store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) } - private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) { + private fun handleSendToken(action: TradeCryptoAction.SendToken) { val currency = action.tokenCurrency val blockchain = Blockchain.fromId(currency.network.id.value) @@ -221,6 +194,17 @@ object TradeCryptoMiddleware { ), ) + val txInfo = action.transactionInfo + if (txInfo != null) { + store.dispatchOnMain( + SendAction.SendSpecificTransaction( + sendAmount = txInfo.amount, + destinationAddress = txInfo.destinationAddress, + transactionId = txInfo.transactionId, + ), + ) + } + val bundle = bundleOf( SendRouter.CRYPTO_CURRENCY_KEY to currency, SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, @@ -229,7 +213,7 @@ object TradeCryptoMiddleware { } } - private fun handleNewSendCoin(action: TradeCryptoAction.New.SendCoin) { + private fun handleSendCoin(action: TradeCryptoAction.SendCoin) { val cryptoStatus = action.coinStatus val currency = cryptoStatus.currency val blockchain = Blockchain.fromId(currency.network.id.value) @@ -278,6 +262,17 @@ object TradeCryptoMiddleware { is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token") } + val txInfo = action.transactionInfo + if (txInfo != null) { + store.dispatchOnMain( + SendAction.SendSpecificTransaction( + sendAmount = txInfo.amount, + destinationAddress = txInfo.destinationAddress, + transactionId = txInfo.transactionId, + ), + ) + } + val bundle = bundleOf( SendRouter.CRYPTO_CURRENCY_KEY to currency, SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt index 5d9fece041..95f2c90b4c 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt @@ -22,20 +22,16 @@ data class MercuryoCurrenciesResponse( ) data class Config( - val base: Map, - @Json(name = "has_withdrawal_fee") - val hasWithdrawalFee: Map, - @Json(name = "display_options") - val displayOptions: Map, - val icons: Map, + @Json(name = "crypto_currencies") + val cryptoCurrencies: List, ) - data class DisplayOption( - @Json(name = "fullname") - val fullName: String, - @Json(name = "total_digits") - val totalDigits: Int, - @Json(name = "display_digits") - val displayDigits: Int, + data class MercuryoCryptoCurrency( + @Json(name = "currency") + val currencySymbol: String, + @Json(name = "network") + val network: String, + @Json(name = "contract") + val contractAddress: String, ) } \ No newline at end of file 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 9446f5c5cf..a19ca0cd84 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 @@ -11,7 +11,6 @@ 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.ExchangeUrlBuilder -import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList /** @@ -21,8 +20,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E private val api: MercuryoApi = environment.mercuryoApi - private val blockchainsAvailableToBuy = CopyOnWriteArrayList() - private val tokensAvailableToBuy = ConcurrentHashMap>() + private val availableMercuryoCurrencies = CopyOnWriteArrayList() override fun featureIsSwitchedOn(): Boolean = true @@ -33,29 +31,14 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E override fun availableForBuy(currency: Currency): Boolean { if (!isBuyAllowed()) return false - // blockchains which cant be defined by mercuryo service - val unsupportedBlockchains = listOf( - Blockchain.Unknown, - Blockchain.Binance, - Blockchain.Arbitrum, - Blockchain.Optimism, - ) - val blockchain = currency.blockchain - - return when (currency) { - is Currency.Blockchain -> { - when { - blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null - unsupportedBlockchains.contains(blockchain) -> false - else -> blockchainsAvailableToBuy.contains(blockchain) - } - } - - is Currency.Token -> { - val supportedInBlockchains = tokensAvailableToBuy[currency.currencySymbol] ?: return false - supportedInBlockchains.contains(blockchain) - } + val mercuryoNetwork = currency.blockchain.mercuryoNetwork() + val contractAddress = (currency as? Currency.Token)?.token?.contractAddress ?: "" + val availableCurrency = availableMercuryoCurrencies.firstOrNull { + it.currencySymbol == currency.currencySymbol && + it.network == mercuryoNetwork && + it.contractAddress == contractAddress } + return availableCurrency != null } override fun availableForSell(currency: Currency): Boolean = false @@ -67,8 +50,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E handleSuccessfullyUpdatedData(data = result.data.data) } result is Result.Failure -> { - blockchainsAvailableToBuy.clear() - tokensAvailableToBuy.clear() + availableMercuryoCurrencies.clear() } } } @@ -95,34 +77,48 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E .appendQueryParameter("return_url", ExchangeUrlBuilder.SUCCESS_URL) if (isDarkTheme) builder.appendQueryParameter("theme", "1inch") + blockchain.mercuryoNetwork()?.let { + builder.appendQueryParameter("network", it) + } + return builder.build().toString() } override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null private fun handleSuccessfullyUpdatedData(data: MercuryoCurrenciesResponse.Data) { - data.crypto.forEach { currencyName -> - val blockchain = blockchainFromCurrencyName(currencyName) - if (blockchain == null) { - val specificBlockchain = data.config.base[currencyName]?.let(::blockchainFromCurrencyName) - if (specificBlockchain != null) { - tokensAvailableToBuy.set( - key = currencyName, - value = tokensAvailableToBuy[currencyName].orEmpty() + specificBlockchain, - ) - } - } else { - blockchainsAvailableToBuy.add(blockchain) - } - } + availableMercuryoCurrencies.clear() + availableMercuryoCurrencies.addAll(data.config.cryptoCurrencies) } - private fun blockchainFromCurrencyName(currencyName: String): Blockchain? { - return when (currencyName) { - "BNB" -> Blockchain.BSC - "ETH" -> Blockchain.Ethereum - "ADA" -> Blockchain.Cardano - else -> Blockchain.values().find { it.currency.lowercase() == currencyName.lowercase() } + @Suppress("CyclomaticComplexMethod") + private fun Blockchain.mercuryoNetwork(): String? { + return when (this) { + // Blockchain.Algorand -> "ALGORAND" //TODO: Uncomment with algo support + Blockchain.Arbitrum -> "ARBITRUM" + Blockchain.Avalanche -> "AVALANCHE" + Blockchain.BSC -> "BINANCESMARTCHAIN" + Blockchain.Bitcoin -> "BITCOIN" + Blockchain.BitcoinCash -> "BITCOINCASH" + Blockchain.Cardano -> "CARDANO" + Blockchain.Cosmos -> "COSMOS" + Blockchain.Dash -> "DASH" + Blockchain.Dogecoin -> "DOGECOIN" + Blockchain.Ethereum -> "ETHEREUM" + Blockchain.Fantom -> "FANTOM" + Blockchain.Kusama -> "KUSAMA" + Blockchain.Litecoin -> "LITECOIN" + Blockchain.Near -> "NEAR_PROTOCOL" + Blockchain.TON -> "NEWTON" + Blockchain.Optimism -> "OPTIMISM" + Blockchain.Polkadot -> "POLKADOT" + Blockchain.Polygon -> "POLYGON" + Blockchain.XRP -> "RIPPLE" + Blockchain.Solana -> "SOLANA" + Blockchain.Stellar -> "STELLAR" + Blockchain.Tezos -> "TEZOS" + Blockchain.Tron -> "TRON" + else -> null } } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 6bd651f70c..b1839fae93 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -1,6 +1,5 @@ package com.tangem.tap.proxy.redux -import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -13,6 +12,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.managetokens.navigation.ManageTokensRouter import com.tangem.features.send.api.featuretoggles.SendFeatureToggles diff --git a/app/src/main/res/drawable/img_alert.xml b/app/src/main/res/drawable/img_alert_80.xml similarity index 100% rename from app/src/main/res/drawable/img_alert.xml rename to app/src/main/res/drawable/img_alert_80.xml diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt index ea8a552360..d3e2fd0694 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt @@ -23,6 +23,9 @@ data class ExpressErrorValue( @Json(name = "minAmount") val minAmount: String?, + @Json(name = "maxAmount") + val maxAmount: String?, + @Json(name = "decimals") val decimals: Int?, diff --git a/core/deep-links/.gitignore b/core/deep-links/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/core/deep-links/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core/deep-links/build.gradle.kts b/core/deep-links/build.gradle.kts new file mode 100644 index 0000000000..9d43ab04df --- /dev/null +++ b/core/deep-links/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.core.deeplink" +} + +dependencies { + + /* Libs - AndroidX */ + implementation(deps.lifecycle.runtime.ktx) + + /* Libs - Other */ + implementation(deps.timber) + + /* DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/core/deep-links/global/.gitignore b/core/deep-links/global/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/core/deep-links/global/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core/deep-links/global/build.gradle.kts b/core/deep-links/global/build.gradle.kts new file mode 100644 index 0000000000..ac47c5ac3d --- /dev/null +++ b/core/deep-links/global/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.core.deeplink.global" +} + +dependencies { + + /* Project */ + implementation(projects.core.deepLinks) +} \ No newline at end of file diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt new file mode 100644 index 0000000000..286f8f8c73 --- /dev/null +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt @@ -0,0 +1,12 @@ +package com.tangem.core.deeplink.global + +import com.tangem.core.deeplink.DeepLink + +class BuyCurrencyDeepLink(val onReceive: () -> Unit) : DeepLink { + + override val uri: String = "tangem://success.tangem.com" + + override fun onReceive(params: Map) { + onReceive() + } +} \ No newline at end of file diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt new file mode 100644 index 0000000000..5d9047a83b --- /dev/null +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt @@ -0,0 +1,24 @@ +package com.tangem.core.deeplink.global + +import com.tangem.core.deeplink.DeepLink + +class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink { + + override val uri: String = "tangem://sell-request.tangem.com" + + override fun onReceive(params: Map) { + val data = Data( + transactionId = params["transactionId"] ?: return, + baseCurrencyAmount = params["baseCurrencyAmount"] ?: return, + depositWalletAddress = params["depositWalletAddress"] ?: return, + ) + + onReceive(data) + } + + data class Data( + val transactionId: String, + val baseCurrencyAmount: String, + val depositWalletAddress: String, + ) +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt new file mode 100644 index 0000000000..0b85327f28 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt @@ -0,0 +1,36 @@ +package com.tangem.core.deeplink + +/** + * Represents a deep link. + */ +interface DeepLink { + + /** + * ID of the deep link. + * + * By default, it is the same as the [uri]. + * */ + val id: String get() = uri + + /** + * URI of the deep link. + * + * **Note: Remember to add the URI in the AndroidManifest.xml file in the `app` module.** + * + * Query parameters will be received automatically. + * + * Path parameters can be added using the following syntax: + * ```kotlin + * "tangem://link" // Without parameters + * "tangem://link/{param1}/{param2}" // With path parameters + * ``` + * */ + val uri: String + + /** + * Method to be called when this deep link is received. + * + * @param params Map of parameters received from the deep link. + * */ + fun onReceive(params: Map) +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt new file mode 100644 index 0000000000..beaeb5f0d2 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt @@ -0,0 +1,63 @@ +package com.tangem.core.deeplink + +import android.content.Intent +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel + +// TODO: Add tests +/** + * Provides functionality to handle deep links. + * + * Allows deep links to be launched, registered, or unregistered. + */ +interface DeepLinksRegistry { + + /** + * Finds matches registered deep links for the given [intent] and launches them. + * + * @return `true` if any deep link was received, `false` otherwise. + */ + fun launch(intent: Intent): Boolean + + /** + * Registers the given [deepLink]. + * + * @see registerWithLifecycle + * @see registerWithViewModel + */ + fun register(deepLink: DeepLink) + + /** + * Registers the given [deepLinks]. + * + * @see registerWithLifecycle + * @see registerWithViewModel + */ + fun register(deepLinks: Collection) + + /** + * Unregisters the given [deepLinks]. + */ + fun unregister(deepLinks: Collection) + + /** + * Unregisters the given [deepLink]. + */ + fun unregister(deepLink: DeepLink) + + /** + * Unregisters deep links with the given [ids]. + * */ + fun unregisterByIds(ids: Collection) + + /** + * Registers the [deepLinks] when the [owner] is resumed and ensures that they are unregistered when the [owner] is + * stopped. + */ + fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection) + + /** + * Registers the [deepLinks] and ensures that they are unregistered when the [ViewModel] is closed. + */ + fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection) +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt new file mode 100644 index 0000000000..4398db12a2 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt @@ -0,0 +1,20 @@ +package com.tangem.core.deeplink.di + +import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.deeplink.impl.DefaultDeepLinksRegistry +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 DeepLinksModule { + + @Provides + @Singleton + fun provideDeepLinksRegistry(): DeepLinksRegistry { + return DefaultDeepLinksRegistry() + } +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt new file mode 100644 index 0000000000..bf84d3e382 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt @@ -0,0 +1,173 @@ +package com.tangem.core.deeplink.impl + +import android.content.Intent +import android.net.Uri +import androidx.core.net.toUri +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel +import com.tangem.core.deeplink.DeepLink +import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.deeplink.utils.DeepLinksLifecycleObserver +import timber.log.Timber + +internal class DefaultDeepLinksRegistry : DeepLinksRegistry { + + private var registries: List = emptyList() + + override fun launch(intent: Intent): Boolean { + val received = intent.data ?: return false + var hasMatch = false + + Timber.d( + """ + Received deep link intent + |- Received URI: $received + |- Registries: $registries + """.trimIndent(), + ) + registries.forEach { deepLink -> + val expected = deepLink.uri.toUri() + + if (!isMatches(expected, received)) return@forEach + hasMatch = true + + val params = getParams(expected, received) + + Timber.d( + """ + Matched deep link + |- Expected URI: $expected + |- Received URI: $received + |- Params: $params + """.trimIndent(), + ) + deepLink.onReceive(params) + } + + if (!hasMatch) { + Timber.d( + """ + No match found for deep link + |- Received URI: $received + |- Registries: $registries + """.trimIndent(), + ) + } + + return hasMatch + } + + override fun register(deepLinks: Collection) { + registries = (registries + deepLinks).distinctBy(DeepLink::id) + + Timber.d( + """ + Registered deep links + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun register(deepLink: DeepLink) { + registries = (registries + deepLink).distinctBy(DeepLink::id) + + Timber.d( + """ + Registered deep link + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun unregister(deepLinks: Collection) { + registries = registries.filter { it !in deepLinks } + + Timber.d( + """ + Unregistered deep links + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun unregister(deepLink: DeepLink) { + registries = registries.filter { it.id != deepLink.id } + + Timber.d( + """ + Unregistered deep link + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun unregisterByIds(ids: Collection) { + registries = registries.filter { it.id !in ids } + + Timber.d( + """ + Unregistered deep links + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection) { + val observer = DeepLinksLifecycleObserver(deepLinksRegistry = this, deepLinks) + owner.lifecycle.addObserver(observer) + } + + override fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection) { + viewModel.addCloseable { + unregister(deepLinks) + } + + register(deepLinks) + } + + private fun isMatches(received: Uri, expected: Uri): Boolean { + if (received == expected) return true + if (received.authority != expected.authority || + received.pathSegments.size != expected.pathSegments.size + ) { + return false + } + + received.pathSegments.forEachIndexed { index, receivedSegment -> + val expectedSegment = expected.pathSegments[index] + if (receivedSegment != expectedSegment && + !(receivedSegment.startsWith(prefix = "{") && receivedSegment.endsWith(suffix = "}")) + ) { + return false + } + } + + return true + } + + private fun getParams(received: Uri, expected: Uri): Map { + val params = mutableMapOf() + + received.pathSegments.forEachIndexed { index, receivedSegment -> + val expectedSegment = expected.pathSegments[index] + if (receivedSegment != expectedSegment && + receivedSegment.startsWith(prefix = "{") && + receivedSegment.endsWith(suffix = "}") + ) { + val path = receivedSegment + .replace(oldValue = "{", newValue = "") + .replace(oldValue = "}", newValue = "") + + params[path] = expectedSegment + } + } + + expected.queryParameterNames.forEach { paramName -> + expected.getQueryParameter(paramName)?.let { param -> + params[paramName] = param + } + } + + return params + } +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/DeepLinksLifecycleObserver.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/DeepLinksLifecycleObserver.kt new file mode 100644 index 0000000000..ae1580e17e --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/DeepLinksLifecycleObserver.kt @@ -0,0 +1,20 @@ +package com.tangem.core.deeplink.utils + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.tangem.core.deeplink.DeepLink +import com.tangem.core.deeplink.DeepLinksRegistry + +internal class DeepLinksLifecycleObserver( + private val deepLinksRegistry: DeepLinksRegistry, + private val deepLinks: Collection, +) : DefaultLifecycleObserver { + + override fun onResume(owner: LifecycleOwner) { + deepLinksRegistry.register(deepLinks) + } + + override fun onPause(owner: LifecycleOwner) { + deepLinksRegistry.unregister(deepLinks) + } +} \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings-blockchain.xml b/core/res/src/main/res/values-de/strings-blockchain.xml index d438a29cd8..82986f8529 100644 --- a/core/res/src/main/res/values-de/strings-blockchain.xml +++ b/core/res/src/main/res/values-de/strings-blockchain.xml @@ -1,6 +1,7 @@ Erhalt der Gebühr fehlgeschlagen + Senden Sie Geld an diese Andresse um ein Konto zu erstellen Minimaler Betrag ist %s Restbestand zu klein Falsche Gebühr diff --git a/core/res/src/main/res/values-fr/strings-blockchain.xml b/core/res/src/main/res/values-fr/strings-blockchain.xml index 0f4d6dccec..97b9e7c7c7 100644 --- a/core/res/src/main/res/values-fr/strings-blockchain.xml +++ b/core/res/src/main/res/values-fr/strings-blockchain.xml @@ -1,6 +1,7 @@ Échec de réception des commissions + Pour créer un compte, envoyez des fonds monétaires à cette adresse Le montant minimal est de %s Le reste est trop petit Commission non valide diff --git a/core/res/src/main/res/values-it/strings-blockchain.xml b/core/res/src/main/res/values-it/strings-blockchain.xml index 338c03f0da..bedc136ed0 100644 --- a/core/res/src/main/res/values-it/strings-blockchain.xml +++ b/core/res/src/main/res/values-it/strings-blockchain.xml @@ -1,6 +1,7 @@ Impossibile ottenere la commissione + Per creare un account, invia fondi a questo indirizzo L\'importo minimo è di %s L\'importo residuo è molto basso Commissione non valida diff --git a/core/res/src/main/res/values-ru/strings-blockchain.xml b/core/res/src/main/res/values-ru/strings-blockchain.xml index 0744b44f6b..c58315db3f 100644 --- a/core/res/src/main/res/values-ru/strings-blockchain.xml +++ b/core/res/src/main/res/values-ru/strings-blockchain.xml @@ -6,8 +6,8 @@ Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт. Произошла ошибка. Код: %s. - Из-за ограничений Kaspa в одну транзакцию может поместиться только %1$d UTXO. Это означает, что вы можете отправить только %2$s или меньше. Вам нужно уменьшить сумму. - Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе. + Для создания аккаунта отправьте средства на этот адрес + Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. Минимальная сумма: %s Сдача слишком мала diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 12777954f8..e257132463 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -70,7 +70,7 @@ биометрическую аутентификацию биометрией Купить - Купить %1$s + Перейти на %1$s Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. Отмена Закрыть @@ -136,6 +136,7 @@ Перевод Я понял Недоступно + Произошла ошибка. Пожалуйста, попробуйте снова. Да Адрес контракта скопирован! Доступные сети @@ -231,6 +232,7 @@ Провайдер Лучший курс Доступно с %s + Доступно до %s Недоступно для этой пары Требуется разрешение Условиями использования @@ -306,6 +308,8 @@ Повторно введите код доступа Код доступа должен состоять не менее чем из 4 символов. Введенные коды доступа не совпадают + Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам + Ошибка активации Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить? Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас. Добавить резервную карту @@ -448,8 +452,9 @@ Сумма Вычесть из суммы отправки Сумма к получению %s + Поддержка Транзакция не выполнена - Причина: %1$s\Код:%2$s + Причина: %1$s\nКод: %2$s %1$s в %2$s Адрес Код назначения @@ -475,10 +480,10 @@ Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств - Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети. - Комиссия превышает баланс - Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. Увеличение комиссии + Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. + Оставить %s XTZ + Отправить все Установлена высокая комиссия Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению @@ -489,7 +494,6 @@ Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции Возможны задержки по транзакции Необязательное - QR код содержит информацию о сумме отправки равной %s Последние Получатель Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов @@ -528,7 +532,6 @@ Подтвердить Ошибка: %s Вы отправляете - Произошла ошибка. Пожалуйста, попробуйте еще раз. Дать разрешение Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств @@ -651,7 +654,7 @@ Dapp не предоставил необходимые данные для открытия сессии WalletConnect Не удалось найти сессию для обработки запроса Сессии WalletConnect - Подключение к Dapps + Подключение к dApps WalletConnect Транзакция успешно подписана и отправлена ​​в Dapp Транзакция успешно подписана и отправлена ​​в блокчейн @@ -690,8 +693,9 @@ Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s Невозможно покрыть комиссию %s Cервис временно недоступен - Пожалуйста, измените сумму для обмена + Пожалуйста, измените сумму для обмена Сумма для обмена должна быть не менее %s + Сумма для обмена должна быть не более %s Возможно, данная карта - образец или подделка Ошибка проверки подлинности На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. diff --git a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml index 607268ec3a..eb43a8cf6f 100644 --- a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml +++ b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml @@ -4,6 +4,7 @@ 遺留資產 獲取費用失敗 由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。 + 要創建帳戶,請將資金發送到此地址 目標帳戶未激活。發送 %s 或更多以激活帳戶 最小數量是 %s 更動太小 diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index c7ef19cf31..6f802b6be1 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -327,7 +327,6 @@ 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。 批准 錯誤: %s - 有錯誤。請再試一遍 賦予權限 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 餘額不足 @@ -426,7 +425,7 @@ Dapp 沒有提供必要的數據來建立 WalletConnect 連接 找不到請求的連接 WalletConnect 連接 - 連結到Dapps + 連結到dApps WalletConnect 交易已成功簽署並發送至 Dapp 交易已成功簽署並發送至區塊鏈 diff --git a/core/res/src/main/res/values/strings-blockchain.xml b/core/res/src/main/res/values/strings-blockchain.xml index bb28ad284c..2aeb56c771 100644 --- a/core/res/src/main/res/values/strings-blockchain.xml +++ b/core/res/src/main/res/values/strings-blockchain.xml @@ -7,7 +7,8 @@ Not enough funds for the transaction. Please top up your account. An error occurred. Code: %s. Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount. - To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely. + To create account send funds to this address + To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely Destination account is not active. Send %s or more to activate the account. Minimum amount is %s Change is too small diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 585432c7dd..dd42088e6b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -68,7 +68,7 @@ biometric authentication biometrics Buy - Buy %1$s + Go to %1$s You have not given access to your camera, please adjust your privacy settings Cancel Close @@ -135,6 +135,7 @@ Transfer I understand Unreachable + There was an error. Please try again. Yes Contract address copied! Available networks @@ -234,6 +235,7 @@ Provider Best rate Available from %s + Available up to %s Unavailable for this pair Permission Required Terms of Use @@ -309,6 +311,8 @@ Re-enter your Access Code Access code must be at least 4 characters long Entered access code didn\'t match the initial access code + Please repeat the operation. The card will be reset to factory settings. + Activation error You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process? The backup process is partly complete. You can\'t exit it now. Add a backup card @@ -447,8 +451,9 @@ Amount Subtract from send amount The recipient will receive %s + Support The transaction is not completed - Reason: %1$s\nCode:%2$s + Reason: %1$s\nCode: %2$s Confirm %1$s at %2$s Address @@ -480,10 +485,10 @@ Network fee coverage Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance - The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance. - Fee exceeds balance - The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. Fee is increased + The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. + Reduce by %s XTZ + No, send all Custom fee is high The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. The included commission exceeds the transfer amount, leading to a negative value @@ -498,11 +503,6 @@ Existential deposit The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s. Optional - Change - Decline - Recipient’s address scanned - Change the entered amount? - QR code contains information about the sending amount equal to %s Please align your QR code with the square to scan it. Ensure you scan %s network address. Recent Recipient @@ -519,6 +519,7 @@ Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while Invalid address Transaction sent + %s (%s) Buy now I have a promo code… Tangem Wallet @@ -545,7 +546,6 @@ Approve Error: %s You swap - There was an error. Please try again. Give Permission Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds @@ -666,7 +666,7 @@ Dapp didn\'t provide essential data to establish WalletConnect session Failed to find session for request WalletConnect Sessions - Connect to Dapps + Connect to dApps WalletConnect The transaction has been successfully signed and sent to the Dapp The transaction has been succesfully signed and sent to the blockchain @@ -705,8 +705,9 @@ To make a transaction you need to deposit some %1$s %2$s Unable to cover %s fee Service temporarily unavailable - Please change the amount to swap + Please change the amount to swap The amount to swap must be at least %s + The amount of tokens to be swapped must not exceed %s This card might be a production sample or counterfeit Authenticity check failed Only %s signatures are left on this card. You must withdraw all of your funds. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index f332e61cbf..658d6af9f6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -7,7 +7,9 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.* +import androidx.compose.material.RadioButton +import androidx.compose.material.RadioButtonDefaults +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember @@ -210,7 +212,7 @@ private fun TangemDialog( modifier = Modifier .background( shape = TangemTheme.shapes.roundedCornersLarge, - color = TangemTheme.colors.background.plain, + color = TangemTheme.colors.background.primary, ) .padding(vertical = TangemTheme.dimens.spacing24), ) { @@ -386,7 +388,7 @@ private fun SelectorDialogContent( selected = index == selectedItemIndex, onClick = onClick, colors = RadioButtonDefaults.colors( - selectedColor = TangemTheme.colors.icon.accent, + selectedColor = TangemTheme.colors.control.checked, unselectedColor = TangemTheme.colors.icon.secondary, ), interactionSource = interactionSource, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt new file mode 100644 index 0000000000..59c9b5143d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -0,0 +1,172 @@ +package com.tangem.core.ui.components.fields + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.TopCenter +import androidx.compose.ui.Alignment.Companion.TopStart +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +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 com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.* +import java.text.DecimalFormat + +/** + * Simple text field for amount input. + * Validates and trims input text using [DecimalFormat]. Formats visual output using [AmountVisualTransformation]. + * Can display aligned placeholder and currency symbol [symbol]. + * + * @param value initial text + * @param decimals number of decimal places + * @param onValueChange callback + * @param textStyle text and placeholder styles + * @param modifier modifier + * @param symbol currency symbol + * @param color text color + * @param placeholderAlignment alignment of placeholder + * @param showPlaceholder show placeholder + * @param keyboardOptions keyboard options + * + * @see [SimpleTextField] for standard text field + */ +@Composable +fun AmountTextField( + value: String, + decimals: Int, + onValueChange: (String) -> Unit, + textStyle: TextStyle, + modifier: Modifier = Modifier, + symbol: String? = null, + color: Color = TangemTheme.colors.text.primary1, + placeholderAlignment: Alignment = TopStart, + showPlaceholder: Boolean = true, + keyboardOptions: KeyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), +) { + val decimalFormat = rememberDecimalFormat() + + val placeholderTextAlign = if (placeholderAlignment == TopCenter) { + TextAlign.Center + } else { + TextAlign.Start + } + SimpleTextField( + value = value, + onValueChange = { newText -> + if (decimalFormat.isValidSymbols(newText)) { + val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals) + onValueChange(trimmed) + } + }, + modifier = modifier + .background(TangemTheme.colors.background.action), + textStyle = textStyle, + color = color, + keyboardOptions = keyboardOptions, + singleLine = true, + visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat), + decorationBox = { innerTextField -> + Box { + if (value.isBlank() && showPlaceholder) { + val placeholder = if (symbol != null) { + decimalFormat.defaultFormat().plus(" $symbol") + } else { + decimalFormat.defaultFormat() + } + Text( + text = placeholder, + style = textStyle, + color = TangemTheme.colors.text.disabled, + textAlign = placeholderTextAlign, + modifier = Modifier + .align(placeholderAlignment), + ) + } + innerTextField() + } + }, + ) +} + +private fun DecimalFormat.isValidSymbols(text: String): Boolean { + return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text) +} + +// region preview +@Preview(locale = "en", showBackground = true, name = "English") +@Preview(locale = "ru", showBackground = true, name = "Russian") +@Composable +private fun AmountTextFieldPreview( + @PreviewParameter(AmountTextFieldPreviewProvider::class) amount: AmountTextFieldPreviewData, +) { + var text by remember { mutableStateOf(amount.value.orEmpty()) } + TangemTheme { + AmountTextField( + value = text, + decimals = amount.decimals, + symbol = amount.symbol, + placeholderAlignment = amount.placeholderAlignment, + showPlaceholder = amount.showPlaceholder, + onValueChange = { text = it }, + textStyle = TangemTheme.typography.h2.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + modifier = Modifier.fillMaxWidth(), + ) + } +} + +private class AmountTextFieldPreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + AmountTextFieldPreviewData( + symbol = "USD", + value = "1000000,123123", + decimals = 3, + placeholderAlignment = TopStart, + showPlaceholder = true, + ), + AmountTextFieldPreviewData( + symbol = null, + value = "1000000.123123", + decimals = 6, + placeholderAlignment = TopStart, + showPlaceholder = false, + ), + AmountTextFieldPreviewData( + symbol = "$", + value = null, + decimals = 2, + showPlaceholder = true, + placeholderAlignment = TopCenter, + ), + AmountTextFieldPreviewData( + symbol = null, + value = null, + decimals = 2, + showPlaceholder = true, + placeholderAlignment = TopStart, + ), + ) +} + +private data class AmountTextFieldPreviewData( + val symbol: String? = "$", + val value: String? = null, + val decimals: Int = 2, + val showPlaceholder: Boolean, + val placeholderAlignment: Alignment, +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index 7e0e1643c6..1e29006979 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -3,14 +3,18 @@ package com.tangem.core.ui.components.fields import androidx.compose.foundation.layout.Box import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference @@ -19,6 +23,7 @@ import com.tangem.core.ui.res.TangemTheme /** * Simple text field with placeholder */ +@Suppress("ReusedModifierInstance") @Composable fun SimpleTextField( value: String, @@ -29,32 +34,72 @@ fun SimpleTextField( visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, color: Color = TangemTheme.colors.text.primary1, + textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), readOnly: Boolean = false, + decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null, ) { - val focusRequester = remember { FocusRequester() } - BasicTextField( - value = value, - onValueChange = onValueChange, - textStyle = TangemTheme.typography.body2.copy(color = color), - cursorBrush = SolidColor(TangemTheme.colors.text.primary1), - singleLine = singleLine, - readOnly = readOnly, - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - decorationBox = { textValue -> - Box { - if (value.isBlank() && placeholder != null) { - Text( - text = placeholder.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.disabled, - modifier = Modifier, - ) - } - textValue() - } - }, - modifier = modifier - .focusRequester(focusRequester), + var textFieldValueState by remember { + mutableStateOf( + TextFieldValue( + text = value, + selection = when { + value.isEmpty() -> TextRange.Zero + else -> TextRange(value.length, value.length) + }, + ), + ) + } + val focusRequester = remember { FocusRequester.Default } + val customTextSelectionColors = TextSelectionColors( + handleColor = TangemTheme.colors.text.secondary, + backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f), ) + + val textFieldValue = textFieldValueState.copy(text = value) + + SideEffect { + if (textFieldValue.selection != textFieldValueState.selection || + textFieldValue.composition != textFieldValueState.composition + ) { + textFieldValueState = textFieldValue + } + } + + var lastTextValue by remember(value) { mutableStateOf(value) } + + CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) { + BasicTextField( + value = textFieldValue, + onValueChange = { newTextFieldValueState -> + textFieldValueState = newTextFieldValueState + + val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text + lastTextValue = newTextFieldValueState.text + + if (stringChangedSinceLastInvocation) { + onValueChange(newTextFieldValueState.text) + } + }, + textStyle = textStyle.copy(color = color), + cursorBrush = SolidColor(TangemTheme.colors.text.primary1), + singleLine = singleLine, + readOnly = readOnly, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + decorationBox = decorationBox ?: { textValue -> + Box { + if (value.isBlank() && placeholder != null) { + Text( + text = placeholder.resolveReference(), + style = textStyle, + color = TangemTheme.colors.text.disabled, + ) + } + textValue() + } + }, + modifier = modifier + .focusRequester(focusRequester), + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt index b3f27e218e..278c631728 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt @@ -5,28 +5,49 @@ 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.core.ui.utils.formatWithThousands +import java.text.DecimalFormat class AmountVisualTransformation( - private val symbol: String, + private val decimals: Int, + private val symbol: String? = null, + private val decimalFormat: DecimalFormat = DecimalFormat(), ) : VisualTransformation { - override fun filter(text: AnnotatedString): TransformedText { - return TransformedText( - buildAnnotatedString { - append(text) - if (text.isNotBlank()) { - append(" ") - append(symbol) - } - }, - object : OffsetMapping { - override fun originalToTransformed(offset: Int): Int { - return text.length - } - override fun transformedToOriginal(offset: Int): Int { - return text.length + override fun filter(text: AnnotatedString): TransformedText { + val formattedText = decimalFormat.formatWithThousands( + text.text, + decimals, + ) + val groupingSymbol = decimalFormat.decimalFormatSymbols.groupingSeparator + return TransformedText( + text = buildAnnotatedString { + append(formattedText) + if (formattedText.isNotEmpty() && symbol != null) { + append(" $symbol") } }, + offsetMapping = OffsetMappingImpl(text.text, formattedText, groupingSymbol), ) } + + private class OffsetMappingImpl( + private val text: String, + private val formattedText: String, + private val gropingSymbol: Char, + ) : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + var noneDigitCount = 0 + var i = 0 + while (i < offset + noneDigitCount) { + if (formattedText.getOrNull(i++) == gropingSymbol) noneDigitCount++ + } + return (offset + noneDigitCount).coerceIn(0, formattedText.length) + } + + override fun transformedToOriginal(offset: Int): Int { + val noneDigitCount = formattedText.take(offset).count { it == gropingSymbol } + return (offset - noneDigitCount).coerceIn(0, text.length) + } + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt new file mode 100644 index 0000000000..d25b9e7bc4 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterAmount.kt @@ -0,0 +1,104 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Input row for entering amount. Manages correct amount format and validation + * + * @param title title reference + * @param text primary text reference + * @param onValueChange text change callback + * @param modifier modifier + * @param titleColor title color + * @param textColor text color + * @param keyboardOptions keyboard options for field + * @param iconRes action icon + * @param iconTint action icon tint + * @param onIconClick click on action icon + * @param showDivider show divider + * + * @see [InputRowDefault] for read only version + * @see InputRowEnter + */ +@Composable +fun InputRowEnterAmount( + title: TextReference, + text: String, + decimals: Int, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + symbol: String? = null, + titleColor: Color = TangemTheme.colors.text.secondary, + textColor: Color = TangemTheme.colors.text.primary1, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + iconRes: Int? = null, + iconTint: Color = TangemTheme.colors.icon.informative, + onIconClick: (() -> Unit)? = null, + showDivider: Boolean = false, +) { + DividerContainer( + modifier = modifier, + showDivider = showDivider, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.caption2, + color = titleColor, + ) + AmountTextField( + value = text, + decimals = decimals, + symbol = symbol, + onValueChange = onValueChange, + color = textColor, + textStyle = TangemTheme.typography.body2, + keyboardOptions = keyboardOptions, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8), + ) + } + iconRes?.let { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = iconTint, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing10, + bottom = TangemTheme.dimens.spacing10, + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { onIconClick?.invoke() }, + ) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt new file mode 100644 index 0000000000..273e0ab9ab --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt @@ -0,0 +1,94 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * `Input Row Enter Info` for entering amount. Manages correct amount format and validation + + * @param title title reference + * @param text primary text reference + * @param onValueChange text change callback + * @param modifier modifier + * @param titleColor title color + * @param textColor text color + * @param isSingleLine text + * @param visualTransformation applied transformation to text + * @param keyboardOptions keyboard options for field + * @param showDivider show divider + * + * @see [InputRowEnterInfo] + * @see Input Row Enter + * @see Input Row Enter Info + */ +@Composable +fun InputRowEnterInfoAmount( + title: TextReference, + text: String, + decimals: Int, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + symbol: String? = null, + info: TextReference? = null, + titleColor: Color = TangemTheme.colors.text.secondary, + textColor: Color = TangemTheme.colors.text.primary1, + infoColor: Color = TangemTheme.colors.text.tertiary, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + showDivider: Boolean = false, +) { + DividerContainer( + modifier = modifier, + showDivider = showDivider, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.caption2, + color = titleColor, + ) + Row { + AmountTextField( + value = text, + decimals = decimals, + symbol = symbol, + onValueChange = onValueChange, + color = textColor, + textStyle = TangemTheme.typography.body2, + keyboardOptions = keyboardOptions, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8) + .weight(1f), + ) + info?.let { + Text( + text = it.resolveReference(), + style = TangemTheme.typography.body2, + color = infoColor, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .align(Alignment.Bottom), + ) + } + } + } + } +} \ No newline at end of file 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 1fbc40be6a..e3170a4467 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 @@ -48,6 +48,7 @@ fun getActiveIconRes(blockchainId: String): Int { "decimal", "decimal/test" -> R.drawable.img_decimal_22 "xdc", "xdc/test" -> R.drawable.img_xdc_22 "vechain", "vechain/test" -> R.drawable.img_vechain_22 + "aptos", "aptos/test" -> R.drawable.img_aptos_22 else -> R.drawable.ic_alert_24 } } @@ -97,6 +98,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "decimal", "decimal/test" -> R.drawable.img_decimal_22 "xdc-network", "xdc-network/test" -> R.drawable.img_xdc_22 "vechain", "vechain/test" -> R.drawable.img_vechain_22 + "aptos", "aptos/test" -> R.drawable.img_aptos_22 else -> R.drawable.ic_alert_24 } } @@ -143,6 +145,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "decimal" -> R.drawable.img_decimal_22 "xdce-crowd-sale" -> R.drawable.img_xdc_22 "vechain" -> R.drawable.img_vechain_22 + "aptos" -> R.drawable.img_aptos_22 else -> R.drawable.ic_alert_24 } } @@ -192,6 +195,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "decimal", "decimal/test" -> R.drawable.ic_decimal_22 "xdc", "xdc/test" -> R.drawable.ic_xdc_22 "vechain", "vechain/test" -> R.drawable.ic_vechain_22 + "aptos", "aptos/test" -> R.drawable.ic_aptos_22 else -> R.drawable.ic_alert_24 } } @@ -241,6 +245,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "decimal", "decimal/test" -> R.drawable.ic_decimal_22 "xdc-network", "xdc-network/test" -> R.drawable.ic_xdc_22 "vechain", "vechain/test" -> R.drawable.ic_vechain_22 + "aptos", "aptos/test" -> R.drawable.ic_aptos_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/TangemColors.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt index e814d46e20..451207be18 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt @@ -42,7 +42,7 @@ class TangemColors internal constructor( warning: Color, attention: Color, accent: Color = TangemColorPalette.Azure, - constantWhite: Color = TangemColorPalette.White, + constant: Color = TangemColorPalette.White, ) { var primary1 by mutableStateOf(primary1) private set @@ -60,7 +60,7 @@ class TangemColors internal constructor( private set var attention by mutableStateOf(attention) private set - var constantWhite by mutableStateOf(constantWhite) + var constantWhite by mutableStateOf(constant) private set fun update(other: Text) { @@ -83,6 +83,7 @@ class TangemColors internal constructor( warning: Color, attention: Color, accent: Color = TangemColorPalette.Azure, + constant: Color = TangemColorPalette.White, ) { var primary1 by mutableStateOf(primary1) private set @@ -118,8 +119,7 @@ class TangemColors internal constructor( primary: Color, secondary: Color, disabled: Color, - positive: Color = TangemColorPalette.Meadow, - positiveDisabled: Color, + positive: Color = TangemColorPalette.Azure, ) { var primary by mutableStateOf(primary) private set @@ -129,15 +129,12 @@ class TangemColors internal constructor( private set var positive by mutableStateOf(positive) private set - var positiveDisabled by mutableStateOf(positiveDisabled) - private set fun update(other: Button) { primary = other.primary secondary = other.secondary disabled = other.disabled positive = other.positive - positiveDisabled = other.positiveDisabled } } @@ -146,9 +143,7 @@ class TangemColors internal constructor( primary: Color, secondary: Color, tertiary: Color, - plain: Color, action: Color, - fade: Color, ) { var primary by mutableStateOf(primary) private set @@ -156,20 +151,14 @@ class TangemColors internal constructor( private set var tertiary by mutableStateOf(tertiary) private set - var plain by mutableStateOf(plain) - private set var action by mutableStateOf(action) private set - var fade by mutableStateOf(fade) - private set fun update(other: Background) { primary = other.primary secondary = other.secondary tertiary = other.tertiary - plain = other.plain action = other.action - fade = other.fade } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index f87f80fe61..78d91538e0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -69,7 +69,7 @@ private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors { secondary = colors.button.primary, secondaryVariant = colors.text.accent, background = colors.background.primary, - surface = colors.background.plain, + surface = colors.background.secondary, error = colors.text.warning, onPrimary = colors.text.primary1, onSecondary = colors.text.primary1, @@ -94,10 +94,10 @@ private fun lightThemeColors(): TangemColors { attention = TangemColorPalette.Tangerine, ), icon = TangemColors.Icon( - primary1 = TangemColorPalette.Black, + primary1 = TangemColorPalette.Dark6, primary2 = TangemColorPalette.White, secondary = TangemColorPalette.Dark2, - informative = TangemColorPalette.Light5, + informative = TangemColorPalette.Dark1, inactive = TangemColorPalette.Light4, warning = TangemColorPalette.Amaranth, attention = TangemColorPalette.Tangerine, @@ -106,15 +106,12 @@ private fun lightThemeColors(): TangemColors { primary = TangemColorPalette.Dark6, secondary = TangemColorPalette.Light2, disabled = TangemColorPalette.Light2, - positiveDisabled = TangemColorPalette.MagicMint, ), background = TangemColors.Background( primary = TangemColorPalette.White, secondary = TangemColorPalette.Light1, tertiary = TangemColorPalette.Light1, - plain = TangemColorPalette.White, action = TangemColorPalette.White, - fade = TangemColorPalette.White, ), control = TangemColors.Control( checked = TangemColorPalette.Dark6, @@ -123,7 +120,7 @@ private fun lightThemeColors(): TangemColors { ), stroke = TangemColors.Stroke( primary = TangemColorPalette.Light2, - secondary = TangemColorPalette.Dark4, + secondary = TangemColorPalette.Dark5, transparency = TangemColorPalette.White, ), field = TangemColors.Field( @@ -149,25 +146,22 @@ private fun darkThemeColors(): TangemColors { icon = TangemColors.Icon( primary1 = TangemColorPalette.White, primary2 = TangemColorPalette.Dark6, - secondary = TangemColorPalette.Dark1, - informative = TangemColorPalette.Dark2, - inactive = TangemColorPalette.Dark4, + secondary = TangemColorPalette.Light5, + informative = TangemColorPalette.Dark1, + inactive = TangemColorPalette.Dark3, warning = TangemColorPalette.Flamingo, attention = TangemColorPalette.Mustard, ), button = TangemColors.Button( - primary = TangemColorPalette.Light4, + primary = TangemColorPalette.Light2, secondary = TangemColorPalette.Dark4, disabled = TangemColorPalette.Dark5, - positiveDisabled = TangemColorPalette.DarkGreen, ), background = TangemColors.Background( primary = TangemColorPalette.Dark6, secondary = TangemColorPalette.Black, tertiary = TangemColorPalette.Dark6, - plain = TangemColorPalette.Black, action = TangemColorPalette.Dark5, - fade = TangemColorPalette.Black, ), control = TangemColors.Control( checked = TangemColorPalette.Azure, @@ -176,7 +170,7 @@ private fun darkThemeColors(): TangemColors { ), stroke = TangemColors.Stroke( primary = TangemColorPalette.Dark4, - secondary = TangemColorPalette.Dark1, + secondary = TangemColorPalette.Dark4, transparency = TangemColorPalette.Dark6, ), field = TangemColors.Field( diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt index f2572e21ff..eb48e1cd50 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt @@ -57,7 +57,7 @@ data class TangemTypography internal constructor( fontSize = 14.sp, fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp), - lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp), + lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp), ), val body1: TextStyle = TextStyle( fontFamily = RobotoFamily, diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt index c361732038..b437a39cd5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt @@ -45,7 +45,7 @@ abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), Compose if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it } .background( - color = TangemTheme.colors.background.plain, + color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.bottomSheet, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index e2d0c8dc42..c5fc3d7442 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -10,6 +10,7 @@ import java.util.Locale object BigDecimalFormatter { const val EMPTY_BALANCE_SIGN = "—" + const val CAN_BE_LOWER_SIGN = "<" private const val TEMP_CURRENCY_CODE = "USD" diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt new file mode 100644 index 0000000000..8cc2cf7bda --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt @@ -0,0 +1,153 @@ +package com.tangem.core.ui.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalConfiguration +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.Locale + +private const val TEXT_CHUNK_THOUSAND = 3 +private const val POINT_SEPARATOR = '.' + +@Composable +fun rememberDecimalFormat(): DecimalFormat { + val locale = LocalConfiguration.current.locale + val decimalSymbols = remember { DecimalFormatSymbols.getInstance(locale) } + + return remember { + DecimalFormat().apply { + decimalFormatSymbols = decimalSymbols + isParseBigDecimal = true + } + } +} + +/** + * Formats input [String] for InputField, to remove wrong symbols, letters etc + * Use [decimals] for cut this number symbols after floating point + * + * Example (with 8 decimals): + * input string - ab123.46377372ab53 + * result string 123.46377372 + */ +fun DecimalFormat.getValidatedNumberWithFixedDecimals(text: String, decimals: Int): String { + val thousandsSeparator = decimalFormatSymbols.groupingSeparator + val decimalSeparator = decimalFormatSymbols.decimalSeparator + + val lastChar = text.lastOrNull() + val trimmedText = if (text.isNotEmpty() && (lastChar == thousandsSeparator || lastChar == POINT_SEPARATOR)) { + text.dropLast(1) + decimalSeparator + } else { + text + } + + if (trimmedText.startsWith("0") && trimmedText.length > 1 && trimmedText[1] != decimalSeparator) { + return "0" + } + + val filteredChars = trimmedText.replace(thousandsSeparator.toString(), "").filterIndexed { index, c -> + val isOneOrZeroPoint = + c == decimalSeparator && index != 0 && trimmedText.count { it == decimalSeparator } <= 1 + val isIndexPointIndex = + c == decimalSeparator && index != 0 && trimmedText.indexOf(decimalSeparator) == index + c.isDigit() || isIndexPointIndex || isOneOrZeroPoint + } + // If dot is present, take first digits before decimal and first decimals digits after decimal + return if (filteredChars.count { it == decimalSeparator } == 1) { + val beforeDecimal = filteredChars.substringBefore(decimalSeparator) + val afterDecimal = filteredChars.substringAfter(decimalSeparator) + beforeDecimal + decimalSeparator + afterDecimal.take(decimals) + } + // If there is no dot, just take all digits + else { + filteredChars + } +} + +/** + * Formats input [text] with grouping and decimal separators. + * Takes into account [decimals] number of digits after floating point. + */ +fun DecimalFormat.formatWithThousands(text: String, decimals: Int): String { + val thousandsSeparator = decimalFormatSymbols.groupingSeparator + val decimalSeparator = decimalFormatSymbols.decimalSeparator + val localizedText = text.replace("[,.]".toRegex(), decimalSeparator.toString()) + return if (localizedText.count { it == decimalSeparator } == 1) { + val beforeDecimal = localizedText.substringBefore(decimalSeparator) + .reversed() + .chunked(TEXT_CHUNK_THOUSAND) + .joinToString(thousandsSeparator.toString()) + .reversed() + val afterDecimal = localizedText.substringAfter(decimalSeparator) + beforeDecimal + decimalSeparator + afterDecimal.take(decimals) + } + // If there is no dot, just take all digits + else { + localizedText.reversed() + .chunked(TEXT_CHUNK_THOUSAND) + .joinToString(thousandsSeparator.toString()) + .reversed() + } +} + +fun DecimalFormat.defaultFormat(): String { + return "0${decimalFormatSymbols.decimalSeparator}00" +} + +/** + * Checks if text input contains extra decimal separators. + * If so, it will return false, otherwise true. + * + * Note: number can contain only one decimal separator. + */ +fun DecimalFormat.checkDecimalSeparatorDuplicate(text: String): Boolean { + val regex = "[${decimalFormatSymbols.decimalSeparator}]".toRegex() + val decimalSeparatorCount = regex.findAll(text).count() + return decimalSeparatorCount <= 1 // only one decimal separator +} + +/** + * Checks if text input contains grouping separators. + * If so, it will return false, otherwise true. + * + * Note: grouping separators are used only for VisualTransformations. + */ +fun DecimalFormat.checkGroupingSeparator(text: String): Boolean { + val regex = "[${decimalFormatSymbols.groupingSeparator}]".toRegex() + val decimalSeparatorCount = regex.findAll(text).count() + return decimalSeparatorCount == 0 // no grouping separator +} + +fun String.parseToBigDecimal(decimals: Int): BigDecimal { + val decimalFormat = DecimalFormat().apply { + decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault()) + isParseBigDecimal = true + maximumFractionDigits = decimals + minimumFractionDigits = decimals + } + return try { + decimalFormat.parse(this) as? BigDecimal ?: BigDecimal.ZERO + } catch (e: Exception) { + BigDecimal.ZERO + } +} + +fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN): String { + val decimalFormat = DecimalFormat().apply { + decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault()) + isParseBigDecimal = true + isGroupingUsed = false + maximumFractionDigits = decimals + minimumFractionDigits = 0 + this.roundingMode = roundingMode + } + + return try { + decimalFormat.format(this) + } catch (e: Exception) { + "" + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/InputNumberFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/InputNumberFormatter.kt index 7ed7c4a7c4..7873012994 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/InputNumberFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/InputNumberFormatter.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.utils import java.text.DecimalFormat +@Deprecated("Deprecated due to unnecessary abstraction. Use methods from DecimalFormatterExt") class InputNumberFormatter( numberFormat: DecimalFormat, ) { diff --git a/core/ui/src/main/res/drawable/ic_aptos_22.xml b/core/ui/src/main/res/drawable/ic_aptos_22.xml new file mode 100644 index 0000000000..5a01778f17 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_aptos_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_aptos_22.xml b/core/ui/src/main/res/drawable/img_aptos_22.xml new file mode 100644 index 0000000000..acfa723f51 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_aptos_22.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt index 5db470b916..3beae6ac03 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt @@ -10,12 +10,16 @@ class PeriodicTask( private val task: suspend () -> Result, private val onSuccess: (T) -> Unit, private val onError: (Throwable) -> Unit, + private val isDelayFirst: Boolean = false, ) { private var isActive: AtomicBoolean = AtomicBoolean(false) suspend fun runTaskWithDelay() { isActive.set(true) + if (isDelayFirst) { + delay(delay) + } while (isActive.get()) { task.invoke() .onSuccess { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index fa7d68bd98..fc4296fdb8 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.tokens.repository +import com.tangem.blockchain.common.Blockchain import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory @@ -7,6 +8,7 @@ import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency @@ -68,6 +70,11 @@ internal class DefaultNetworksRepository( networksStatusesStore.getSyncOrNull(userWalletId).orEmpty() } + override fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean { + val blockchain = Blockchain.fromNetworkId(network.id.value) + return blockchain == Blockchain.Aptos + } + private suspend fun fetchNetworksStatusesIfCacheExpired( userWalletId: UserWalletId, networks: Set, diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultFeeRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultFeeRepository.kt new file mode 100644 index 0000000000..25ee54c0b0 --- /dev/null +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultFeeRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.data.transaction + +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.FeeRepository + +internal class DefaultFeeRepository : FeeRepository { + override fun isFeeApproximate(networkId: Network.ID, amountType: AmountType): Boolean { + val blockchain = Blockchain.fromId(networkId.value) + return blockchain.isFeeApproximate(amountType) + } +} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index e5cf1cec6c..7c515253c2 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -7,10 +7,7 @@ import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.TransactionExtras +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository @@ -44,6 +41,21 @@ internal class DefaultTransactionRepository( ) } + override suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ) = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + (walletManager as TransactionSender).send(txData, signer) + } + private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { val blockchain = Blockchain.fromId(networkId) if (memo == null) return null diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt index de935654bb..c8949d39c0 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -1,6 +1,8 @@ package com.tangem.data.transaction.di +import com.tangem.data.transaction.DefaultFeeRepository import com.tangem.data.transaction.DefaultTransactionRepository +import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -25,4 +27,10 @@ internal object TransactionDataModule { coroutineDispatcherProvider = coroutineDispatcherProvider, ) } + + @Provides + @Singleton + fun providesFeeRepository(): FeeRepository { + return DefaultFeeRepository() + } } \ No newline at end of file diff --git a/data/visa/.gitignore b/data/visa/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/visa/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts new file mode 100644 index 0000000000..e57696df79 --- /dev/null +++ b/data/visa/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.visa" +} + +dependencies { + + /** Project - Domain */ + implementation(projects.domain.visa) + implementation(projects.domain.wallets.models) + + /** DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt new file mode 100644 index 0000000000..40815fac3c --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt @@ -0,0 +1,12 @@ +package com.tangem.data.visa + +import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.visa.repository.VisaRepository +import com.tangem.domain.wallets.models.UserWalletId + +internal class DummyVisaRepository : VisaRepository { + + override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency { + TODO(reason = "Implement in [REDACTED_JIRA]") + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt new file mode 100644 index 0000000000..ab68249dd9 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -0,0 +1,20 @@ +package com.tangem.data.visa.di + +import com.tangem.data.visa.DummyVisaRepository +import com.tangem.domain.visa.repository.VisaRepository +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 VisaDataModule { + + @Provides + @Singleton + fun provideVisaRepository(): VisaRepository { + return DummyVisaRepository() + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index f4376a8ef5..0cbee25c28 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -24,6 +24,12 @@ interface CardTypesResolver { fun isBadWallet(): Boolean + fun isJrWallet(): Boolean + + fun isGrimWallet(): Boolean + + fun isSatoshiFriendsWallet(): Boolean + fun isWhiteWallet(): Boolean fun isWallet2(): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index f05317c4e6..077ffe5de1 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -42,6 +42,12 @@ internal class TangemCardTypesResolver( override fun isBadWallet(): Boolean = card.batchId == BAD_WALLET_BATCH_ID + override fun isJrWallet(): Boolean = card.batchId == JR_WALLET_BATCH_ID + + override fun isGrimWallet(): Boolean = card.batchId == GRIM_WALLET_BATCH_ID + + override fun isSatoshiFriendsWallet(): Boolean = card.batchId == SATOSHI_WALLET_BATCH_ID + override fun isWhiteWallet(): Boolean { return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } @@ -66,7 +72,10 @@ internal class TangemCardTypesResolver( override fun isSingleWalletWithToken(): Boolean = walletData?.token != null && !isMultiwalletAllowed() override fun isMultiwalletAllowed(): Boolean { - return !isTangemTwins() && !card.isStart2Coin && !isTangemNote() && + return !isTangemTwins() && + !card.isStart2Coin && + !isTangemNote() && + !isVisaWallet() && (multiWalletAvailable() || card.wallets.firstOrNull()?.curve == EllipticCurve.Secp256k1) } @@ -75,6 +84,7 @@ internal class TangemCardTypesResolver( override fun getBlockchain(): Blockchain { return when (productType) { ProductType.Start2Coin -> if (card.isTestCard) Blockchain.BitcoinTestnet else Blockchain.Bitcoin + ProductType.Visa -> Blockchain.PolygonTestnet else -> { val blockchainName: String = walletData?.blockchain ?: if (productType == ProductType.Note) { @@ -138,6 +148,9 @@ internal class TangemCardTypesResolver( const val TRON_WALLET_BATCH_ID = "AF07" const val KASPA_WALLET_BATCH_ID = "AF08" const val BAD_WALLET_BATCH_ID = "AF09" + const val JR_WALLET_BATCH_ID = "AF14" + const val GRIM_WALLET_BATCH_ID = "AF13" + const val SATOSHI_WALLET_BATCH_ID = "AF19" const val WHITE_WALLET2_BATCH_ID = "AF15" const val TRILLIANT_WALLET_BATCH_ID = "AF16" const val AVRORA_WALLET_BATCH_ID = "AF18" diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index b6f3d8aba6..9d0357e1f1 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -225,6 +225,7 @@ fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? { Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(NODL_AMOUNT_TO_CREATE_ACCOUNT) else BigDecimal.ONE Blockchain.XRP -> BigDecimal.TEN Blockchain.Near, Blockchain.NearTestnet -> 0.00182.toBigDecimal() + Blockchain.Aptos, Blockchain.AptosTestnet -> BigDecimal.ZERO else -> null } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt index f715061ac7..826ba5a966 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt @@ -13,4 +13,9 @@ sealed interface LegacyAction : Action { * BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup */ data class StartOnboardingProcess(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : LegacyAction + + /** + * Sending an email to support when sending transaction failed + */ + data class SendEmailTransactionFailed(val errorMessage: String) : LegacyAction } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Amount.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Amount.kt new file mode 100644 index 0000000000..eb777dbcca --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Amount.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +data class Amount( + val currencySymbol: String, + val value: BigDecimal? = null, + val decimals: Int, + val type: AmountType = AmountType.CoinType, +) + +sealed class AmountType { + object CoinType : AmountType() + object ReserveType : AmountType() + data class TokenType(val token: CryptoCurrency.Token) : AmountType() + data class FiatType(val code: String) : AmountType() +} + +/** Converts `BigDecimal` [cryptoCurrency] to [Amount] */ +fun BigDecimal.convertToAmount(cryptoCurrency: CryptoCurrency) = Amount( + currencySymbol = cryptoCurrency.symbol, + value = this, + decimals = cryptoCurrency.decimals, + type = when (cryptoCurrency) { + is CryptoCurrency.Coin -> AmountType.CoinType + is CryptoCurrency.Token -> AmountType.TokenType( + token = cryptoCurrency, + ) + }, +) \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index f0acf4eef2..e393e4fbd3 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -30,6 +30,8 @@ sealed class CryptoCurrencyWarning { val amountCurrency: CryptoCurrency, ) : CryptoCurrencyWarning() + object TopUpWithoutReserve : CryptoCurrencyWarning() + /** * Represents wallet blockchain rent * @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt new file mode 100644 index 0000000000..b1f054e498 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt @@ -0,0 +1,97 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.FeePaidCurrency +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +/** + * Use case for getting balance not enough warning to cover fee. + * + * This warning is shown when current currency is not paying fee and paying fee currency balance is not enough + * + * Current | Paying fee | Warning + * Coin | Coin | - + * Token | Coin | + + * Coin | PToken | + (VTO - VTHO) + * Token | PToken | + (Other VeChainToken - VTHO) + * PToken | PToken | - (VTHO - VTHO or TerraToken - TerraToken) + */ +class GetBalanceNotEnoughForFeeWarningUseCase( + private val currenciesRepository: CurrenciesRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + suspend operator fun invoke( + fee: BigDecimal, + userWalletId: UserWalletId, + tokenStatus: CryptoCurrencyStatus, + coinStatus: CryptoCurrencyStatus, + ): Either = Either.catch { + withContext(dispatchers.io) { + val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency) + val coinBalance = coinStatus.value.amount ?: BigDecimal.ZERO + + val isFeePaidByCoin = tokenStatus.currency is CryptoCurrency.Token + val isFeePaidByToken = + feePaidCurrency is FeePaidCurrency.Token && tokenStatus.currency.id != feePaidCurrency.tokenId + + val warning = when { + feePaidCurrency is FeePaidCurrency.Coin && isFeePaidByCoin && fee > coinBalance -> { + CryptoCurrencyWarning.BalanceNotEnoughForFee( + tokenCurrency = tokenStatus.currency, + coinCurrency = coinStatus.currency, + ) + } + feePaidCurrency is FeePaidCurrency.Token && isFeePaidByToken && fee > feePaidCurrency.balance -> { + constructTokenBalanceNotEnoughWarning( + userWalletId = userWalletId, + tokenStatus = tokenStatus, + feePaidToken = feePaidCurrency, + ) + } + else -> null + } + warning + } + } + + /** + * Check if fee paying token [feePaidToken] is added to wallet [userWalletId] + */ + private suspend fun constructTokenBalanceNotEnoughWarning( + userWalletId: UserWalletId, + tokenStatus: CryptoCurrencyStatus, + feePaidToken: FeePaidCurrency.Token, + ): CryptoCurrencyWarning { + val token = currenciesRepository + .getMultiCurrencyWalletCurrenciesSync(userWalletId) + .find { + it is CryptoCurrency.Token && + it.contractAddress.equals(feePaidToken.contractAddress, ignoreCase = true) && + it.network.derivationPath == tokenStatus.currency.network.derivationPath + } + return if (token != null) { + CryptoCurrencyWarning.CustomTokenNotEnoughForFee( + currency = tokenStatus.currency, + feeCurrency = token, + networkName = token.network.name, + feeCurrencyName = feePaidToken.name, + feeCurrencySymbol = feePaidToken.symbol, + ) + } else { + CryptoCurrencyWarning.CustomTokenNotEnoughForFee( + currency = tokenStatus.currency, + feeCurrency = null, + networkName = tokenStatus.currency.network.name, + feeCurrencyName = feePaidToken.name, + feeCurrencySymbol = feePaidToken.symbol, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt index ffe6e79c3b..ed3cbfad6c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt @@ -1,8 +1,8 @@ package com.tangem.domain.tokens import arrow.core.Either -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.error.mapper.mapToTokenListError +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations @@ -22,7 +22,7 @@ class GetCryptoCurrencyStatusSyncUseCase( suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, - ): Either { + ): Either { val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, currenciesRepository = currenciesRepository, @@ -31,6 +31,18 @@ class GetCryptoCurrencyStatusSyncUseCase( ) return operations.getCurrencyStatusSync(cryptoCurrencyId) - .mapLeft { error -> error.mapToTokenListError() } + .mapLeft { error -> error.mapToCurrencyError() } + } + + suspend operator fun invoke(userWalletId: UserWalletId): Either { + val operations = CurrenciesStatusesOperations( + userWalletId = userWalletId, + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + ) + + return operations.getPrimaryCurrencyStatusSync() + .mapLeft { error -> error.mapToCurrencyError() } } } \ No newline at end of file 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 3545e10ba9..b43052898e 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 @@ -254,10 +254,14 @@ class GetCurrencyWarningsUseCase( private fun getNetworkNoAccountWarning(currencyStatus: CryptoCurrencyStatus): CryptoCurrencyWarning? { return (currencyStatus.value as? CryptoCurrencyStatus.NoAccount)?.let { - CryptoCurrencyWarning.SomeNetworksNoAccount( - amountToCreateAccount = it.amountToCreateAccount, - amountCurrency = currencyStatus.currency, - ) + if (networksRepository.isNeedToCreateAccountWithoutReserve(network = currencyStatus.currency.network)) { + CryptoCurrencyWarning.TopUpWithoutReserve + } else { + CryptoCurrencyWarning.SomeNetworksNoAccount( + amountToCreateAccount = it.amountToCreateAccount, + amountCurrency = currencyStatus.currency, + ) + } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt new file mode 100644 index 0000000000..d003a95067 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.FeePaidCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +/** + * Use case for checking if currency amount can be subtracted. + * Amount can be subtracted if only it is paying fee + */ +class IsAmountSubtractAvailableUseCase( + private val currenciesRepository: CurrenciesRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either = + Either.catch { + withContext(dispatchers.io) { + when (val feeCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, currency)) { + is FeePaidCurrency.Coin -> currency is CryptoCurrency.Coin + is FeePaidCurrency.SameCurrency -> true + is FeePaidCurrency.Token -> currency.id == feeCurrency.tokenId + } + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 85abbb2bc8..1657a3ea05 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -8,43 +8,41 @@ import java.math.BigDecimal sealed class TradeCryptoAction : Action { - data class SendCrypto( - val currencyId: String, + data class FinishSelling(val transactionId: String) : TradeCryptoAction() + + data class Buy( + val userWallet: UserWallet, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrencyCode: String, + val checkUserLocation: Boolean = true, + ) : TradeCryptoAction() + + data class Sell( + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrencyCode: String, + ) : TradeCryptoAction() + + data class SendToken( + val userWallet: UserWallet, + val tokenCurrency: CryptoCurrency.Token, + val tokenFiatRate: BigDecimal?, + val coinFiatRate: BigDecimal?, + val feeCurrencyStatus: CryptoCurrencyStatus?, + val transactionInfo: TransactionInfo? = null, + ) : TradeCryptoAction() + + data class SendCoin( + val userWallet: UserWallet, + val coinStatus: CryptoCurrencyStatus, + val feeCurrencyStatus: CryptoCurrencyStatus?, + val transactionInfo: TransactionInfo? = null, + ) : TradeCryptoAction() + + data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction() + + data class TransactionInfo( val amount: String, val destinationAddress: String, val transactionId: String, - ) : TradeCryptoAction() - - data class FinishSelling(val transactionId: String) : TradeCryptoAction() - - sealed class New : TradeCryptoAction() { - - data class Buy( - val userWallet: UserWallet, - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val appCurrencyCode: String, - val checkUserLocation: Boolean = true, - ) : New() - - data class Sell( - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val appCurrencyCode: String, - ) : New() - - data class SendToken( - val userWallet: UserWallet, - val tokenCurrency: CryptoCurrency.Token, - val tokenFiatRate: BigDecimal?, - val coinFiatRate: BigDecimal?, - val feeCurrencyStatus: CryptoCurrencyStatus?, - ) : New() - - data class SendCoin( - val userWallet: UserWallet, - val coinStatus: CryptoCurrencyStatus, - val feeCurrencyStatus: CryptoCurrencyStatus?, - ) : New() - - data class Swap(val cryptoCurrency: CryptoCurrency) : New() - } + ) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index a86698763b..92d6969de0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -11,6 +11,8 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* +// FIXME: Refactor - [REDACTED_JIRA] +@Suppress("LargeClass") internal class CurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, @@ -107,6 +109,27 @@ internal class CurrenciesStatusesOperations( } } + suspend fun getPrimaryCurrencyStatusSync(): Either = either { + val currency = catch( + block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, + catch = { raise(Error.DataError(it)) }, + ) + val quotes = catch( + block = { quotesRepository.getQuoteSync(currency.id).right() }, + catch = { Error.DataError(it).left() }, + ) + val networkStatus = catch( + block = { + networksRepository.getNetworkStatusesSync(userWalletId, setOf(currency.network)) + .firstOrNull { it.network == currency.network } + .right() + }, + catch = { Error.DataError(it).left() }, + ) + + return createCurrencyStatus(currency, quotes, networkStatus) + } + fun getCardCurrenciesStatusesFlow(): Flow>> { return flow { val nonEmptyCurrencies = recover( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 8e1b1103ff..6ec3fbb11a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -41,6 +41,8 @@ interface NetworksRepository { suspend fun getNetworkStatusesSync( userWalletId: UserWalletId, networks: Set, - refresh: Boolean, + refresh: Boolean = false, ): Set + + fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index f345dbedeb..ecf1d2a23b 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -32,4 +32,6 @@ internal class MockNetworksRepository( ): Set { return getNetworkStatusesUpdates(userWalletId, networks).first() } + + override fun isNeedToCreateAccountWithoutReserve(network: Network) = false } \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index 571b6c0f50..f0345df888 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -13,9 +13,13 @@ dependencies { implementation(deps.arrow.core) implementation(projects.core.utils) + implementation(projects.core.ui) /** Tangem SDKs */ implementation(deps.tangem.card.core) + implementation(deps.tangem.card.android) { + exclude(module = "joda-time") + } implementation(deps.tangem.blockchain) implementation(projects.domain.models) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/FeeRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/FeeRepository.kt new file mode 100644 index 0000000000..9434d8cf80 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/FeeRepository.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.transaction + +import com.tangem.blockchain.common.AmountType +import com.tangem.domain.tokens.model.Network + +interface FeeRepository { + + /** Returns if fee is approximate for current [networkId] */ + fun isFeeApproximate(networkId: Network.ID, amountType: AmountType): Boolean +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 6c38bc0e7d..70c8b7d0ae 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -1,8 +1,10 @@ package com.tangem.domain.transaction import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.CommonSigner import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -17,4 +19,11 @@ interface TransactionRepository { userWalletId: UserWalletId, network: Network, ): TransactionData? + + suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ): SimpleResult } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt index 8ed9705902..2d5536cefb 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -1,5 +1,7 @@ package com.tangem.domain.transaction.error +import com.tangem.core.ui.extensions.TextReference + sealed class SendTransactionError { object DemoCardError : SendTransactionError() @@ -8,9 +10,12 @@ sealed class SendTransactionError { data class NetworkError(val message: String?) : SendTransactionError() - data class BlockchainSdkError(val code: Int, val cause: Throwable?) : SendTransactionError() + data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError() + object UserCancelledError : SendTransactionError() - data class TangemSdkError(val code: Int, val cause: Throwable?) : SendTransactionError() + + data class TangemSdkError(val code: Int, val messageReference: TextReference) : SendTransactionError() + data class UnknownError(val ex: Exception? = null) : SendTransactionError() companion object { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt index e27a7d2625..ae697ceb08 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt @@ -1,21 +1,15 @@ package com.tangem.domain.transaction.usecase -import arrow.core.Either -import arrow.core.left -import arrow.core.right +import arrow.core.raise.catch +import arrow.core.raise.either import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn import java.math.BigDecimal /** @@ -23,16 +17,15 @@ import java.math.BigDecimal */ class GetFeeUseCase( private val walletManagersFacade: WalletManagersFacade, - private val dispatcher: CoroutineDispatcherProvider, ) { suspend operator fun invoke( amount: BigDecimal, destination: String, userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Flow> { - return flow { - try { + ) = either { + catch( + block = { val result = requireNotNull( walletManagersFacade.getFee( amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount), @@ -43,14 +36,15 @@ class GetFeeUseCase( ) { "Fee is null" } val maybeFee = when (result) { - is Result.Success -> result.data.right() - is Result.Failure -> GetFeeError.DataError(result.error).left() + is Result.Success -> result.data + is Result.Failure -> raise(GetFeeError.DataError(result.error)) } - emit(maybeFee) - } catch (e: Exception) { - emit(GetFeeError.DataError(e.cause).left()) - } - }.flowOn(dispatcher.io) + maybeFee + }, + catch = { + raise(GetFeeError.DataError(it)) + }, + ) } private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount( diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsFeeApproximateUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsFeeApproximateUseCase.kt new file mode 100644 index 0000000000..688716716f --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsFeeApproximateUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.transaction.usecase + +import com.tangem.blockchain.common.AmountType +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.FeeRepository + +/** + * Use case to check if fee is approximate + * + * @param feeRepository [FeeRepository] + */ +class IsFeeApproximateUseCase( + private val feeRepository: FeeRepository, +) { + + operator fun invoke(networkId: Network.ID, amountType: AmountType): Boolean { + return feeRepository.isFeeApproximate(networkId, amountType) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 1e710033ae..c8ed1fe905 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -8,19 +8,23 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.network.ResultChecker import com.tangem.common.core.TangemSdkError +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.R +import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet +import com.tangem.sdk.extensions.localizedDescriptionRes class SendTransactionUseCase( private val isDemoCardUseCase: IsDemoCardUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, - private val walletManagersFacade: WalletManagersFacade, + private val transactionRepository: TransactionRepository, ) { suspend operator fun invoke( txData: TransactionData, @@ -37,7 +41,7 @@ class SendTransactionUseCase( if (isDemoCardUseCase(cardId = userWallet.cardId)) { SendTransactionError.DemoCardError.left() } else { - walletManagersFacade.sendTransaction( + transactionRepository.sendTransaction( txData = txData, signer = signer, userWalletId = userWallet.walletId, @@ -64,29 +68,28 @@ class SendTransactionUseCase( private fun handleError(result: SimpleResult.Failure): SendTransactionError { if (ResultChecker.isNetworkError(result)) return SendTransactionError.NetworkError(result.error.message) val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError() - when (error) { + return when (error) { is BlockchainSdkError.WrappedTangemError -> { - val errorByCode = mapErrorByCode(error) - if (errorByCode != null) { - return errorByCode + if (error.code == USER_CANCELLED_ERROR_CODE) { + SendTransactionError.UserCancelledError + } else { + val tangemError = error.tangemError + if (tangemError is TangemSdkError) { + val resource = tangemError.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + val textReference = resourceReference(resId, wrappedList(resArgs)) + SendTransactionError.TangemSdkError(tangemError.code, textReference) + } else { + SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) + } } - val tangemSdkError = error.tangemError as? TangemSdkError ?: return SendTransactionError.UnknownError() - if (tangemSdkError is TangemSdkError.UserCancelled) return SendTransactionError.UserCancelledError - return SendTransactionError.TangemSdkError(tangemSdkError.code, tangemSdkError.cause) } else -> { - return SendTransactionError.TangemSdkError(error.code, error.cause) - } - } - } - - private fun mapErrorByCode(error: BlockchainSdkError.WrappedTangemError): SendTransactionError? { - return when (error.code) { - USER_CANCELLED_ERROR_CODE -> { - return SendTransactionError.UserCancelledError - } - else -> { - null + SendTransactionError.BlockchainSdkError( + code = error.code, + message = error.customMessage, + ) } } } diff --git a/domain/visa/.gitignore b/domain/visa/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/visa/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts new file mode 100644 index 0000000000..64262e878a --- /dev/null +++ b/domain/visa/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.visa" +} + +dependencies { + + /** Project - Domain */ + implementation(projects.core.utils) + implementation(projects.domain.core) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.appCurrency.models) + + /** Libs - Other */ + implementation(deps.jodatime) +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/GetVisaCurrencyUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/GetVisaCurrencyUseCase.kt new file mode 100644 index 0000000000..d5b71eff26 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/GetVisaCurrencyUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.visa + +import arrow.core.Either +import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.visa.repository.VisaRepository +import com.tangem.domain.wallets.models.UserWalletId + +class GetVisaCurrencyUseCase( + private val repository: VisaRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + isRefresh: Boolean = false, + ): Either { + return Either.catch { repository.getVisaCurrency(userWalletId, isRefresh) } + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt new file mode 100644 index 0000000000..1e6a0c55a1 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt @@ -0,0 +1,32 @@ +package com.tangem.domain.visa.model + +import com.tangem.domain.appcurrency.model.AppCurrency +import org.joda.time.DateTime +import java.math.BigDecimal + +data class VisaCurrency( + val networkName: String, + val symbol: String, + val decimals: Int, + val fiatRate: BigDecimal?, + val fiatCurrency: AppCurrency, + val balances: Balances, + val limits: Limits, +) { + + data class Balances( + val total: BigDecimal, + val verified: BigDecimal, + val available: BigDecimal, + val blocked: BigDecimal, + val debt: BigDecimal, + val pendingRefund: BigDecimal, + ) + + data class Limits( + val remainingOtp: BigDecimal, + val remainingNoOtp: BigDecimal, + val singleTransaction: BigDecimal, + val expirationDate: DateTime, + ) +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaRepository.kt new file mode 100644 index 0000000000..96d6efd7fd --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaRepository.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.visa.repository + +import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.wallets.models.UserWalletId + +interface VisaRepository { + + suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean = false): VisaCurrency +} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt similarity index 97% rename from features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt rename to features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt index 75e0acda9f..978f1ea01e 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt @@ -14,7 +14,7 @@ import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseSc /** [REDACTED_AUTHOR] */ -class OnboardingSeedPhrase : OnboardingSeedPhraseApi { +class OnboardingSeedPhraseScreen : OnboardingSeedPhraseApi { @Composable override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) { diff --git a/features/send/api/build.gradle.kts b/features/send/api/build.gradle.kts index 6b742b5542..eb561fcf20 100644 --- a/features/send/api/build.gradle.kts +++ b/features/send/api/build.gradle.kts @@ -10,7 +10,6 @@ android { } dependencies { - implementation(projects.domain.tokens.models) /** AndroidX */ implementation(deps.androidx.fragment.ktx) diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 53a6d8a573..46295abadb 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(deps.lifecycle.compose) implementation(deps.jodatime) implementation(deps.timber) + implementation(deps.reKotlin) /** Compose */ implementation(deps.compose.accompanist.systemUiController) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 360165f228..db243390ea 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -23,6 +23,7 @@ internal class DefaultSendRouter( } override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { + reduxNavController.popBackStack() reduxNavController.navigate( action = NavigationAction.NavigateTo( screen = AppScreen.WalletDetails, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt new file mode 100644 index 0000000000..54eb2f7868 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt @@ -0,0 +1,52 @@ +package com.tangem.features.send.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.send.impl.R + +@Immutable +internal sealed class SendAlertState { + + abstract val title: TextReference? + abstract val message: TextReference + open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + open val onConfirmClick: (() -> Unit)? = null + + data class GenericError( + override val title: TextReference? = resourceReference(id = R.string.send_alert_transaction_failed_title), + override val onConfirmClick: (() -> Unit), + ) : SendAlertState() { + override val message: TextReference = resourceReference(R.string.common_unknown_error) + override val confirmButtonText: TextReference = + resourceReference(id = R.string.send_alert_button_request_support) + } + + data class TransactionError( + val code: String, + val cause: String?, + val causeTextReference: TextReference? = null, + override val onConfirmClick: (() -> Unit), + ) : SendAlertState() { + override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title) + override val message: TextReference = resourceReference( + id = R.string.send_alert_transaction_failed_text, + formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code), + ) + override val confirmButtonText: TextReference = + resourceReference(id = R.string.send_alert_button_request_support) + } + + data class DemoMode( + override val onConfirmClick: () -> Unit, + ) : SendAlertState() { + override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title) + override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message) + } + + object FeeIncreased : SendAlertState() { + override val title: TextReference? = null + override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEvent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEvent.kt new file mode 100644 index 0000000000..2fdb687d96 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEvent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class SendEvent { + + data class ShowSnackBar(val text: TextReference) : SendEvent() + + data class ShowAlert(val alert: SendAlertState) : SendEvent() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt new file mode 100644 index 0000000000..a175478e41 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt @@ -0,0 +1,88 @@ +package com.tangem.features.send.impl.presentation.state + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory +import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider +import java.math.BigDecimal + +/** + * Factory to produce event state for [SendUiState] + * + * @param currentStateProvider [Provider] of [SendUiState] + * @param clickIntents [SendClickIntents] + * @param feeStateFactory [FeeStateFactory] + */ +internal class SendEventStateFactory( + private val currentStateProvider: Provider, + private val clickIntents: SendClickIntents, + private val feeStateFactory: FeeStateFactory, +) { + private val sendTransactionErrorConverter by lazy { SendTransactionAlertConverter(clickIntents) } + + fun onConsumeEventState(): SendUiState { + return currentStateProvider().copy(event = consumedEvent()) + } + + fun getSendTransactionErrorState(error: SendTransactionError?, onConsume: () -> Unit): SendUiState { + val state = currentStateProvider() + val event = error?.let { + sendTransactionErrorConverter.convert(error)?.let { + triggeredEvent(SendEvent.ShowAlert(it), onConsume) + } + } + return state.copy( + event = event ?: consumedEvent(), + ) + } + + fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit, onFeeNotIncreased: () -> Unit): SendUiState { + val state = currentStateProvider() + val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state + val newFee = when (fee) { + is TransactionFee.Single -> fee.normal + is TransactionFee.Choosable -> { + when (feeSelector.selectedFee) { + FeeType.SLOW -> fee.minimum + FeeType.MARKET -> fee.normal + FeeType.FAST -> fee.priority + FeeType.CUSTOM -> return state + } + } + } + + val newFeeValue = newFee.amount.value ?: BigDecimal.ZERO + val oldFeeValue = feeStateFactory.feeConverter.convert(feeSelector).amount.value ?: BigDecimal.ZERO + val updateFeeState = feeStateFactory.onFeeOnLoadedState(fee) + return if (newFeeValue > oldFeeValue) { + updateFeeState.copy( + event = triggeredEvent( + data = SendEvent.ShowAlert(SendAlertState.FeeIncreased), + onConsume = onConsume, + ), + ) + } else { + onFeeNotIncreased() + updateFeeState + } + } + + fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState { + val state = currentStateProvider() + return state.copy( + event = triggeredEvent( + data = SendEvent.ShowAlert( + SendAlertState.GenericError( + onConfirmClick = { clickIntents.onFailedTxEmailClick(error?.localizedMessage.orEmpty()) }, + ), + ), + onConsume = onConsume, + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index 6d2975319a..cdbd005127 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -57,16 +57,28 @@ internal sealed class SendNotification(val config: NotificationConfig) { sealed class Warning( title: TextReference, subtitle: TextReference, + buttonsState: NotificationConfig.ButtonsState? = null, ) : SendNotification( config = NotificationConfig( title = title, subtitle = subtitle, iconResId = R.drawable.img_attention_20, + buttonsState = buttonsState, ), ) { - data class HighFeeError(val amount: String) : Warning( + data class HighFeeError( + val amount: String, + val onConfirmClick: () -> Unit, + val onDismissClick: () -> Unit, + ) : Warning( title = resourceReference(R.string.send_notification_high_fee_title), subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)), + buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig( + primaryText = resourceReference(R.string.send_notification_fee_too_high_accept, wrappedList(amount)), + onPrimaryClick = onConfirmClick, + secondaryText = resourceReference(R.string.send_notification_fee_too_high_ignore), + onSecondaryClick = onDismissClick, + ), ) data class ExistentialDeposit(val deposit: String) : Warning( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt index ce29bd2c60..18e0b4f34c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt @@ -6,6 +6,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -21,6 +22,7 @@ internal class SendNotificationFactory( private val currentStateProvider: Provider, private val userWalletProvider: Provider, private val walletManagersFacade: WalletManagersFacade, + private val clickIntents: SendClickIntents, ) { fun create(): Flow> = currentStateProvider().currentState @@ -30,19 +32,33 @@ internal class SendNotificationFactory( val feeState = state.feeState ?: return@map persistentListOf() val recipientState = state.recipientState ?: return@map persistentListOf() val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO + val amountValue = state.amountState?.amountTextField?.value?.toBigDecimalOrNull() ?: BigDecimal.ZERO + val sendAmount = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue buildList { // errors - addExceedBalanceNotification(feeAmount, feeState.receivedAmountValue) - addInvalidAmountNotification(feeState.isSubtract, feeState.receivedAmountValue) - addMinimumAmountErrorNotification(feeAmount, feeState.receivedAmountValue) + addExceedBalanceNotification(feeAmount, sendAmount) + addInvalidAmountNotification(feeState.isSubtract, sendAmount) + addMinimumAmountErrorNotification(feeAmount, sendAmount) addReserveAmountErrorNotification(recipientState.addressTextField.value) - addTransactionLimitErrorNotification(feeAmount, feeState.receivedAmountValue) + addTransactionLimitErrorNotification(feeAmount, sendAmount) // warnings - addExistentialWarningNotification(feeAmount, feeState.receivedAmountValue) - addHighFeeWarningNotification() + addExistentialWarningNotification(feeAmount, sendAmount) + addHighFeeWarningNotification(amountValue, state.sendState.ignoreAmountReduce) }.toImmutableList() } + fun dismissHighFeeWarningState(): SendUiState { + val state = currentStateProvider() + val sendState = state.sendState + val updatedNotifications = sendState.notifications.filterNot { it is SendNotification.Warning.HighFeeError } + return state.copy( + sendState = sendState.copy( + ignoreAmountReduce = true, + notifications = updatedNotifications.toImmutableList(), + ), + ) + } + private fun MutableList.addExceedBalanceNotification( feeAmount: BigDecimal, receivedAmount: BigDecimal, @@ -173,16 +189,30 @@ internal class SendNotificationFactory( } } - private fun MutableList.addHighFeeWarningNotification() { - // TODO Move Blockchain check elsewhere - if (cryptoCurrencyStatusProvider().currency.network.id.value == Blockchain.Tezos.id) { - add(SendNotification.Warning.HighFeeError(TEZOS_FEE_THRESHOLD)) + private fun MutableList.addHighFeeWarningNotification( + sendAmount: BigDecimal, + ignoreAmountReduce: Boolean, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id + if (!ignoreAmountReduce && sendAmount == balance && isTezos) { + add( + SendNotification.Warning.HighFeeError( + amount = TEZOS_FEE_THRESHOLD.toPlainString(), + onConfirmClick = { + val reduceTo = sendAmount.minus(TEZOS_FEE_THRESHOLD).toPlainString() + clickIntents.onAmountReduceClick(reduceTo) + }, + onDismissClick = clickIntents::onAmountReduceIgnoreClick, + ), + ) } } companion object { private const val CARDANO_MINIMUM = "1" private const val DOGECOIN_MINIMUM = "0.01" - private const val TEZOS_FEE_THRESHOLD = "0.01" + private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01") } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 70fac6ad0d..17ca733eee 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -3,10 +3,9 @@ package com.tangem.features.send.impl.presentation.state import androidx.paging.PagingData import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryItem @@ -15,23 +14,21 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.AvailableWallet +import com.tangem.features.send.impl.presentation.state.amount.SendAmountCurrencyConverter import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter -import com.tangem.features.send.impl.presentation.state.fee.* +import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider -import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import timber.log.Timber -import java.math.BigDecimal -@Suppress("LongParameterList", "LargeClass") +@Suppress("LongParameterList") internal class SendStateFactory( private val clickIntents: SendClickIntents, private val currentStateProvider: Provider, @@ -40,25 +37,28 @@ internal class SendStateFactory( private val cryptoCurrencyStatusProvider: Provider, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, - coinCryptoCurrencyStatusProvider: Provider, ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) - - private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) } - private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) } - private val customFeeFieldConverter by lazy { - SendFeeCustomFieldConverter( + private val amountFieldConverter by lazy { + SendAmountFieldConverter( clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, appCurrencyProvider = appCurrencyProvider, ) } - - private val feeNotificationFactory = FeeNotificationFactory( - coinCryptoCurrencyStatusProvider = coinCryptoCurrencyStatusProvider, - userWalletProvider = userWalletProvider, - clickIntents = clickIntents, - ) + private val amountFieldChangeConverter by lazy { + SendAmountFieldChangeConverter( + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + private val amountCurrencyConverter by lazy { + SendAmountCurrencyConverter( + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } private val amountStateConverter by lazy { SendAmountStateConverter( @@ -77,6 +77,7 @@ internal class SendStateFactory( } private val feeStateConverter by lazy { SendFeeStateConverter( + appCurrencyProvider = appCurrencyProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } @@ -92,6 +93,7 @@ internal class SendStateFactory( fun getInitialState(): SendUiState = SendUiState( clickIntents = clickIntents, currentState = MutableStateFlow(SendUiStateType.Amount), + event = consumedEvent(), ) fun getReadyState(): SendUiState { @@ -107,16 +109,7 @@ internal class SendStateFactory( //region amount state clicks fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value) - fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState { - val state = currentStateProvider() - val amountState = state.amountState ?: return state - - return if (amountState.isFiatValue == isFiat) { - state - } else { - return state.copy(amountState = amountState.copy(isFiatValue = isFiat)) - } - } + fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat) //endregion //region recipient @@ -218,144 +211,6 @@ internal class SendStateFactory( } //endregion - //region fee - fun onFeeOnLoadingState(): SendUiState { - val state = currentStateProvider() - val feeState = state.feeState ?: return state - val feeSelectorState = FeeSelectorState.Loading - return state.copy( - feeState = feeState.copy( - feeSelectorState = feeSelectorState, - notifications = persistentListOf(), - isPrimaryButtonEnabled = feeSelectorState.isPrimaryButtonEnabled(), - ), - ) - } - - fun onFeeOnLoadedState(fees: TransactionFee): SendUiState { - val state = currentStateProvider() - val feeState = state.feeState ?: return state - val feeSelectorState = FeeSelectorState.Content( - fees = fees, - customValues = customFeeFieldConverter.convert(fees.normal), - ) - - val fee = feeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract) - val updatedState = feeState.copy( - feeSelectorState = feeSelectorState, - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - ) - return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory(feeState = updatedState), - isPrimaryButtonEnabled = feeSelectorState.isPrimaryButtonEnabled(), - ), - ) - } - - fun onFeeSelectedState(feeType: FeeType): SendUiState { - val state = currentStateProvider() - val feeState = state.feeState ?: return state - val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state - - val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType) - val fee = updatedFeeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract) - - val updatedState = feeState.copy( - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - feeSelectorState = updatedFeeSelectorState, - isPrimaryButtonEnabled = updatedFeeSelectorState.isPrimaryButtonEnabled(), - ) - - return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory(feeState = updatedState), - ), - ) - } - - fun onCustomFeeValueChange(index: Int, value: String): SendUiState { - val state = currentStateProvider() - val feeState = state.feeState ?: return state - val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state - val updatedFeeSelectorState = feeSelectorState.copy( - customValues = feeSelectorState.customValues.toMutableList().apply { - set(index, feeSelectorState.customValues[index].copy(value = value)) - }.toImmutableList(), - ) - - val fee = updatedFeeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee, feeState.isSubtract) - - val updatedState = feeState.copy( - feeSelectorState = updatedFeeSelectorState, - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - isPrimaryButtonEnabled = updatedFeeSelectorState.isPrimaryButtonEnabled(), - ) - return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory(feeState = updatedState), - ), - ) - } - - fun onSubtractSelect(value: Boolean): SendUiState { - val state = currentStateProvider() - val feeState = state.feeState ?: return state - val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state - - val fee = feeSelectorState.getFee() - val receivedAmount = calculateReceiveAmount(state, fee, value) - val updatedState = feeState.copy( - isSubtract = value, - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = if (value) { - getFormattedValue(receivedAmount) - } else { - feeState.receivedAmount - }, - ) - return state.copy( - feeState = updatedState.copy( - notifications = feeNotificationFactory(feeState = updatedState), - ), - ) - } - - private fun FeeSelectorState.isPrimaryButtonEnabled(): Boolean { - return when (this) { - is FeeSelectorState.Loading -> false - is FeeSelectorState.Content -> { - val customValue = customValues.firstOrNull()?.value?.toBigDecimalOrNull() - val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO - val fee = getFee().amount.value ?: BigDecimal.ZERO - - val isNotEmptyCustom = !customValue.isNullOrZero() && selectedFee == FeeType.CUSTOM - val isNotCustom = selectedFee != FeeType.CUSTOM - fee < balance && (isNotEmptyCustom || isNotCustom) - } - } - } - - private fun getFormattedValue(value: BigDecimal): String { - val cryptoCurrency = cryptoCurrencyStatusProvider().currency - return BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = value, - cryptoCurrency = cryptoCurrency.symbol, - decimals = cryptoCurrency.decimals, - ) - } - //endregion - //region send fun getSendingStateUpdate(isSending: Boolean): SendUiState { val state = currentStateProvider() @@ -375,6 +230,7 @@ internal class SendStateFactory( transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(), isSuccess = true, txUrl = txUrl, + notifications = persistentListOf(), ), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt new file mode 100644 index 0000000000..14f9cc265d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt @@ -0,0 +1,44 @@ +package com.tangem.features.send.impl.presentation.state + +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter + +internal class SendTransactionAlertConverter( + private val clickIntents: SendClickIntents, +) : Converter { + override fun convert(value: SendTransactionError): SendAlertState? { + return when (value) { + SendTransactionError.DemoCardError -> SendAlertState.DemoMode( + onConfirmClick = { clickIntents.popBackStack() }, + ) + is SendTransactionError.TangemSdkError -> SendAlertState.TransactionError( + code = value.code.toString(), + cause = null, + causeTextReference = value.messageReference, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.code.toString()) }, + ) + is SendTransactionError.BlockchainSdkError -> SendAlertState.TransactionError( + code = value.code.toString(), + cause = value.message, + onConfirmClick = { clickIntents.onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") }, + ) + is SendTransactionError.DataError -> SendAlertState.TransactionError( + code = "", + cause = value.message, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.NetworkError -> SendAlertState.TransactionError( + code = "", + cause = value.message, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) }, + ) + is SendTransactionError.UnknownError -> SendAlertState.TransactionError( + code = "", + cause = value.ex?.localizedMessage, + onConfirmClick = { clickIntents.onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) }, + ) + else -> null + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index fb2918c11d..16d0b6190a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -5,8 +5,9 @@ import androidx.compose.runtime.Stable import androidx.paging.PagingData import com.tangem.blockchain.common.transaction.Fee import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState @@ -31,6 +32,7 @@ internal data class SendUiState( val sendState: SendStates.SendState = SendStates.SendState(), val recipientList: MutableStateFlow> = MutableStateFlow(PagingData.empty()), val currentState: MutableStateFlow, + val event: StateEvent, ) @Stable @@ -41,20 +43,19 @@ internal sealed class SendStates { abstract val isPrimaryButtonEnabled: Boolean /** Amount state */ + @Stable data class AmountState( override val type: SendUiStateType = SendUiStateType.Amount, override val isPrimaryButtonEnabled: Boolean, - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val appCurrency: AppCurrency, val walletName: String, - val walletBalance: String, + val walletBalance: TextReference, val tokenIconState: TokenIconState, - val isFiatValue: Boolean, val segmentedButtonConfig: PersistentList, - val amountTextField: SendTextField.Amount, + val amountTextField: SendTextField.AmountField, ) : SendStates() /** Recipient state */ + @Stable data class RecipientState( override val type: SendUiStateType = SendUiStateType.Recipient, override val isPrimaryButtonEnabled: Boolean, @@ -66,19 +67,25 @@ internal sealed class SendStates { ) : SendStates() /** Fee and speed state */ + @Stable data class FeeState( override val type: SendUiStateType = SendUiStateType.Fee, override val isPrimaryButtonEnabled: Boolean = false, - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val feeSelectorState: FeeSelectorState = FeeSelectorState.Loading, - val isSubtract: Boolean = false, - val fee: Fee? = null, - val receivedAmountValue: BigDecimal = BigDecimal.ZERO, - val receivedAmount: String = "", - val notifications: ImmutableList = persistentListOf(), + val feeSelectorState: FeeSelectorState, + val isSubtractAvailable: Boolean, + val isSubtract: Boolean, + val isUserSubtracted: Boolean, + val fee: Fee?, + val receivedAmountValue: BigDecimal, + val receivedAmount: String, + val rate: BigDecimal?, + val appCurrency: AppCurrency, + val isFeeApproximate: Boolean, + val notifications: ImmutableList, ) : SendStates() /** Send state */ + @Stable data class SendState( override val type: SendUiStateType = SendUiStateType.Send, override val isPrimaryButtonEnabled: Boolean = true, @@ -86,6 +93,7 @@ internal sealed class SendStates { val isSuccess: Boolean = false, val transactionDate: Long = 0L, val txUrl: String = "", + val ignoreAmountReduce: Boolean = false, val notifications: ImmutableList = persistentListOf(), ) : SendStates() } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index 97ad5f2dfc..4edb445226 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -11,22 +11,16 @@ internal class StateRouter( var currentState: MutableStateFlow = MutableStateFlow(SendUiStateType.Recipient) private set - private var isFromSend: Boolean = false - fun popBackStack() { fragmentManager.get()?.popBackStack() } fun onBackClick() { - if (isFromSend) { - showSend() - } else { - when (currentState.value) { - SendUiStateType.Recipient -> popBackStack() - SendUiStateType.Amount -> showRecipient() - SendUiStateType.Fee -> showAmount() - SendUiStateType.Send -> showFee() - } + when (currentState.value) { + SendUiStateType.Recipient -> popBackStack() + SendUiStateType.Amount -> showRecipient() + SendUiStateType.Fee -> showAmount() + SendUiStateType.Send -> showFee() } } @@ -48,18 +42,15 @@ internal class StateRouter( } } - fun showAmount(isFromSend: Boolean = false) { - this.isFromSend = isFromSend + fun showAmount() { currentState.update { SendUiStateType.Amount } } - fun showRecipient(isFromSend: Boolean = false) { - this.isFromSend = isFromSend + fun showRecipient() { currentState.update { SendUiStateType.Recipient } } - fun showFee(isFromSend: Boolean = false) { - this.isFromSend = isFromSend + fun showFee() { currentState.update { SendUiStateType.Fee } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt new file mode 100644 index 0000000000..0f95e9fad7 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt @@ -0,0 +1,32 @@ +package com.tangem.features.send.impl.presentation.state.amount + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero + +internal class SendAmountCurrencyConverter( + private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + override fun convert(value: Boolean): SendUiState { + val state = currentStateProvider() + val amountState = state.amountState ?: return state + val amountTextField = amountState.amountTextField + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() + return if (amountTextField.isFiatValue == value && !isValidFiatRate) { + state + } else { + return state.copy( + amountState = amountState.copy( + amountTextField = amountTextField.copy( + isFiatValue = value, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt index c80815c345..5bda8cc991 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt @@ -1,12 +1,15 @@ package com.tangem.features.send.impl.presentation.state.amount import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +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.utils.BigDecimalFormatter.formatCryptoAmount import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.utils.Provider @@ -29,13 +32,10 @@ internal class SendAmountStateConverter( val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals) return SendStates.AmountState( - appCurrency = appCurrency, - cryptoCurrencyStatus = status, walletName = userWallet.name, - walletBalance = "$crypto ($fiat)", + walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)), tokenIconState = iconStateConverter.convert(status), amountTextField = sendAmountFieldConverter.convert(Unit), - isFiatValue = false, isPrimaryButtonEnabled = false, segmentedButtonConfig = persistentListOf( SendAmountSegmentedButtonsConfig( 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 1805ce3f6a..16d58d39a6 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 @@ -1,44 +1,14 @@ package com.tangem.features.send.impl.presentation.state.fee import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.features.send.impl.presentation.state.SendUiState import java.math.BigDecimal /** * Calculate receiving amount when fee is subtracted from sending amount */ -internal fun calculateReceiveAmount(uiState: SendUiState, feeAmount: Fee, isSubtract: Boolean): BigDecimal { - val amount = uiState.amountState?.amountTextField?.value ?: return BigDecimal.ZERO +internal fun calculateReceiveAmount(state: SendUiState, feeAmount: Fee): BigDecimal { + val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO val fee = feeAmount.amount.value ?: return BigDecimal.ZERO - return if (isSubtract) { - amount.toBigDecimal().minus(fee) - } else { - amount.toBigDecimal() - } -} - -/** - * Returns fee amount depending on current state - */ -internal fun FeeSelectorState.Content.getFee(): Fee { - return when (fees) { - is TransactionFee.Choosable -> { - when (selectedFee) { - FeeType.SLOW -> fees.minimum - FeeType.MARKET -> fees.normal - FeeType.FAST -> fees.priority - FeeType.CUSTOM -> { - val feeAmount = - customValues.firstOrNull()?.value?.let { BigDecimal(it.ifEmpty { "0" }) } ?: BigDecimal.ZERO - Fee.Common( - fees.normal.amount.copy( - value = feeAmount, - ), - ) - } - } - } - is TransactionFee.Single -> fees.normal - } + return amountValue.minus(fee) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt new file mode 100644 index 0000000000..c14db17575 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt @@ -0,0 +1,60 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter + +internal class FeeConverter( + private val clickIntents: SendClickIntents, + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + + private val ethereumCustomFeeConverter by lazy { + EthereumCustomFeeConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + + override fun convert(value: FeeSelectorState.Content): Fee { + return when (val fees = value.fees) { + is TransactionFee.Choosable -> { + when (value.selectedFee) { + FeeType.SLOW -> fees.minimum + FeeType.MARKET -> fees.normal + FeeType.FAST -> fees.priority + FeeType.CUSTOM -> convertCustom(value, fees) + } + } + is TransactionFee.Single -> fees.normal + } + } + + private fun convertCustom(feeSelectorState: FeeSelectorState.Content, fees: TransactionFee): Fee { + val customValues = feeSelectorState.customValues + val normalFee = fees.normal + return if (customValues.isEmpty()) { + normalFee + } else { + when (normalFee) { + is Fee.Ethereum -> ethereumCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues) + else -> { + val customFee = customValues.firstOrNull() + Fee.Common( + normalFee.amount.copy( + value = customFee?.value?.parseToBigDecimal(customFee.decimals), + ), + ) + } + } + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt index fa63899356..244ed17cdd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt @@ -1,35 +1,63 @@ package com.tangem.features.send.impl.presentation.state.fee +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider -import kotlinx.collections.immutable.ImmutableList +import com.tangem.utils.toFormattedString import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import java.math.BigDecimal internal class FeeNotificationFactory( private val coinCryptoCurrencyStatusProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val currentStateProvider: Provider, private val userWalletProvider: Provider, private val clickIntents: SendClickIntents, + private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, ) { - operator fun invoke(feeState: SendStates.FeeState): ImmutableList { - val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return persistentListOf() - val customFee = feeSelectorState.customValues - val selectedFee = feeSelectorState.selectedFee + fun create() = currentStateProvider().currentState + .filter { it == SendUiStateType.Fee } + .map { + val state = currentStateProvider() + val feeState = state.feeState ?: return@map persistentListOf() + buildList { + when (val feeSelectorState = feeState.feeSelectorState) { + FeeSelectorState.Loading -> Unit + FeeSelectorState.Error -> { + addFeeUnreachableNotification(feeSelectorState) + } + is FeeSelectorState.Content -> { + val customFee = feeSelectorState.customValues + val selectedFee = feeSelectorState.selectedFee + addTooLowNotification(feeSelectorState.fees, selectedFee, customFee) + addTooHighNotification(feeSelectorState.fees, selectedFee, customFee) + addFeeCoverageNotification(feeState, state.amountState) + addExceedsBalanceNotification(feeState.fee) + } + } + }.toImmutableList() + } - return buildList { - addTooLowNotification(feeSelectorState.fees, selectedFee, customFee) - addTooHighNotification(feeSelectorState.fees, selectedFee, customFee) - addFeeCoverageNotification() - addExceedsBalanceNotification(feeSelectorState) - }.toImmutableList() + private fun MutableList.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) { + if (feeSelectorState is FeeSelectorState.Error) { + add(SendFeeNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload)) + } } private fun MutableList.addTooLowNotification( @@ -39,7 +67,8 @@ internal class FeeNotificationFactory( ) { val multipleFees = transactionFee as? TransactionFee.Choosable ?: return val minimumValue = multipleFees.minimum.amount.value ?: return - val customValue = customFee.firstOrNull()?.value?.toBigDecimalOrNull() ?: return + val customAmount = customFee.firstOrNull() ?: return + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) if (selectedFee == FeeType.CUSTOM && minimumValue > customValue) { add(SendFeeNotification.Informational.TooLow) } @@ -52,46 +81,87 @@ internal class FeeNotificationFactory( ) { val multipleFees = transactionFee as? TransactionFee.Choosable ?: return val highValue = multipleFees.priority.amount.value ?: return - val customValue = customFee.firstOrNull()?.value?.toBigDecimalOrNull() ?: return + val customAmount = customFee.firstOrNull() ?: return + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) val diff = customValue / highValue if (selectedFee == FeeType.CUSTOM && diff > FEE_MAX_DIFF) { - add(SendFeeNotification.Warning.TooHigh(diff.toInt().toString())) + add(SendFeeNotification.Warning.TooHigh(diff.toFormattedString(HIGH_FEE_DIFF_DECIMALS))) } } - private fun MutableList.addFeeCoverageNotification() { - // TODO add fee coverage condition [REDACTED_JIRA] - add(SendFeeNotification.Warning.NetworkCoverage) + private fun MutableList.addFeeCoverageNotification( + feeState: SendStates.FeeState, + amountState: SendStates.AmountState?, + ) { + if (!feeState.isSubtractAvailable) return + + val cryptoAmount = coinCryptoCurrencyStatusProvider().value.amount ?: return + val feeValue = feeState.fee?.amount?.value ?: return + val value = amountState?.amountTextField?.cryptoAmount?.value ?: return + if (cryptoAmount <= value + feeValue && feeState.isSubtract && !feeState.isUserSubtracted) { + add(SendFeeNotification.Warning.NetworkCoverage) + } } - private fun MutableList.addExceedsBalanceNotification( - feeSelectorState: FeeSelectorState.Content, - ) { - val coinCryptoCurrency = coinCryptoCurrencyStatusProvider() - val cryptoAmount = coinCryptoCurrency.value.amount ?: BigDecimal.ZERO - val choosableFee = feeSelectorState.fees as? TransactionFee.Choosable - val fee = when (feeSelectorState.selectedFee) { - FeeType.SLOW -> choosableFee?.minimum?.amount?.value - FeeType.MARKET -> feeSelectorState.fees.normal.amount.value - FeeType.FAST -> choosableFee?.priority?.amount?.value - FeeType.CUSTOM -> feeSelectorState.customValues.firstOrNull()?.value?.toBigDecimalOrNull() - } ?: return + private suspend fun MutableList.addExceedsBalanceNotification(fee: Fee?) { + val feeValue = fee?.amount?.value ?: BigDecimal.ZERO + val userWalletId = userWalletProvider().walletId + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - if (fee > cryptoAmount) { - add( - SendFeeNotification.Error.ExceedsBalance( - coinCryptoCurrency.currency.networkIconResId, - ) { - clickIntents.onTokenDetailsClick( - userWalletProvider().walletId, - coinCryptoCurrency.currency, - ) - }, - ) + val warning = getBalanceNotEnoughForFeeWarningUseCase( + fee = feeValue, + userWalletId = userWalletId, + tokenStatus = cryptoCurrencyStatus, + coinStatus = coinCryptoCurrencyStatusProvider(), + ).fold( + ifLeft = { null }, + ifRight = { it }, + ) ?: return + + when (warning) { + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> { + add( + SendFeeNotification.Error.ExceedsBalance( + warning.coinCurrency.networkIconResId, + networkName = warning.coinCurrency.name, + currencyName = cryptoCurrencyStatus.currency.name, + feeName = warning.coinCurrency.name, + feeSymbol = warning.coinCurrency.symbol, + onClick = { + clickIntents.onTokenDetailsClick( + userWalletId = userWalletId, + currency = warning.coinCurrency, + ) + }, + ), + ) + } + is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { + val currency = warning.feeCurrency + add( + SendFeeNotification.Error.ExceedsBalance( + networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24, + currencyName = warning.currency.name, + feeName = warning.feeCurrencyName, + feeSymbol = warning.feeCurrencySymbol, + networkName = warning.networkName, + onClick = currency?.let { + { + clickIntents.onTokenDetailsClick( + userWalletId, + currency, + ) + } + }, + ), + ) + } + else -> Unit } } companion object { private val FEE_MAX_DIFF = BigDecimal(5) + private const val HIGH_FEE_DIFF_DECIMALS = 0 } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt index c5a392c4a9..9751e61eae 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt @@ -16,6 +16,8 @@ internal sealed class FeeSelectorState { val selectedFee: FeeType = FeeType.MARKET, val customValues: ImmutableList = persistentListOf(), ) : FeeSelectorState() + + object Error : FeeSelectorState() } enum class FeeType { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt new file mode 100644 index 0000000000..fcbfd66705 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -0,0 +1,237 @@ +package com.tangem.features.send.impl.presentation.state.fee + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.extensions.isZero +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +/** + * Factory to produce fee state for [SendUiState] + */ +internal class FeeStateFactory( + private val clickIntents: SendClickIntents, + private val currentStateProvider: Provider, + private val coinCryptoCurrencyStatusProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val isFeeApproximateUseCase: IsFeeApproximateUseCase, +) { + private val customFeeFieldConverter by lazy { + SendFeeCustomFieldConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + + val feeConverter by lazy { + FeeConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + + fun onFeeOnLoadingState(): SendUiState { + val state = currentStateProvider() + val feeState = state.feeState ?: return state + return state.copy( + feeState = feeState.copy( + feeSelectorState = FeeSelectorState.Loading, + notifications = persistentListOf(), + isPrimaryButtonEnabled = false, + ), + ) + } + + fun onFeeOnLoadedState(fees: TransactionFee, isSubtractAvailable: Boolean): SendUiState { + val state = currentStateProvider() + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val feeState = state.feeState ?: return state + val feeSelectorState = (feeState.feeSelectorState as? FeeSelectorState.Content)?.copy( + fees = fees, + customValues = customFeeFieldConverter.convert(fees.normal), + ) ?: FeeSelectorState.Content( + fees = fees, + customValues = customFeeFieldConverter.convert(fees.normal), + ) + + val fee = feeConverter.convert(feeSelectorState) + val receivedAmount = calculateReceiveAmount(state, fee) + return state.copy( + feeState = feeState.copy( + isSubtractAvailable = isSubtractAvailable, + feeSelectorState = feeSelectorState, + fee = fee, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = isSubtractAvailable && checkAutoSubtract(state, fee, balance), + isFeeApproximate = isFeeApproximate(fee), + ), + ) + } + + fun onFeeOnLoadedState(fees: TransactionFee): SendUiState { + val state = currentStateProvider() + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val feeState = state.feeState ?: return state + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state + + val updatedFeeSelector = feeSelectorState.copy( + fees = fees, + customValues = customFeeFieldConverter.convert(fees.normal), + ) + val fee = feeConverter.convert(updatedFeeSelector) + val receivedAmount = calculateReceiveAmount(state, fee) + return state.copy( + feeState = feeState.copy( + feeSelectorState = updatedFeeSelector, + fee = fee, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), + ), + ) + } + + fun onFeeOnErrorState(): SendUiState { + val state = currentStateProvider() + return state.copy( + feeState = state.feeState?.copy( + feeSelectorState = FeeSelectorState.Error, + ), + ) + } + + fun onFeeSelectedState(feeType: FeeType): SendUiState { + val state = currentStateProvider() + val feeState = state.feeState ?: return state + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + + val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType) + val fee = feeConverter.convert(updatedFeeSelectorState) + val receivedAmount = calculateReceiveAmount(state, fee) + return state.copy( + feeState = feeState.copy( + fee = fee, + feeSelectorState = updatedFeeSelectorState, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), + ), + ) + } + + fun onCustomFeeValueChange(index: Int, value: String): SendUiState { + val state = currentStateProvider() + val feeState = state.feeState ?: return state + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state + val updatedFeeSelectorState = customFeeFieldConverter.onValueChange(feeSelectorState, index, value) + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + + val fee = feeConverter.convert(updatedFeeSelectorState) + val receivedAmount = calculateReceiveAmount(state, fee) + return state.copy( + feeState = feeState.copy( + feeSelectorState = updatedFeeSelectorState, + fee = fee, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + isSubtract = checkAutoSubtract(state, fee, balance), + ), + ) + } + + fun onSubtractSelect(value: Boolean): SendUiState { + val state = currentStateProvider() + val feeState = state.feeState ?: return state + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state + val fee = feeConverter.convert(feeSelectorState) + val receivedAmount = calculateReceiveAmount(state, fee) + return state.copy( + feeState = feeState.copy( + isSubtract = value, + isUserSubtracted = true, + receivedAmountValue = receivedAmount, + receivedAmount = getFormattedValue(receivedAmount), + fee = fee, + ), + ) + } + + fun getFeeNotificationState(notifications: ImmutableList): SendUiState { + val state = currentStateProvider() + return state.copy( + feeState = state.feeState?.copy( + notifications = notifications, + isPrimaryButtonEnabled = isPrimaryButtonEnabled(state.feeState, notifications), + ), + ) + } + + private fun isPrimaryButtonEnabled( + feeState: SendStates.FeeState, + notifications: ImmutableList, + ): Boolean { + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return false + val customValue = feeSelectorState.customValues.firstOrNull() + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val fee = feeConverter.convert(feeSelectorState) + val feeValue = fee.amount.value ?: BigDecimal.ZERO + + val isNotCustom = feeSelectorState.selectedFee != FeeType.CUSTOM + val isNotEmptyCustom = if (customValue != null) { + !customValue.value.parseToBigDecimal(customValue.decimals).isZero() && !isNotCustom + } else { + false + } + val noErrors = notifications.none { it is SendFeeNotification.Error } + val isSubtractRequired = when { + !feeState.isSubtractAvailable -> true // current currency is not fee currency + feeValue + feeState.receivedAmountValue >= balance -> feeState.isSubtract + else -> feeValue + feeState.receivedAmountValue <= balance + } + + return noErrors && isSubtractRequired && (isNotEmptyCustom || isNotCustom) + } + + private fun checkAutoSubtract(state: SendUiState, fee: Fee, balance: BigDecimal): Boolean { + val feeState = state.feeState ?: return false + val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO + val feeAmount = fee.amount.value ?: BigDecimal.ZERO + return if (feeState.isUserSubtracted) { + feeState.isSubtract + } else { + amountValue + feeAmount >= balance + } + } + + private fun isFeeApproximate(fee: Fee): Boolean { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + return isFeeApproximateUseCase( + networkId = cryptoCurrencyStatus.currency.network.id, + amountType = fee.amount.type, + ) + } + + private fun getFormattedValue(value: BigDecimal): String { + val cryptoCurrency = cryptoCurrencyStatusProvider().currency + return BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = value, + cryptoCurrency = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt index ff9da4a71f..24c85ee305 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -1,12 +1,9 @@ package com.tangem.features.send.impl.presentation.state.fee -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType import com.tangem.blockchain.common.transaction.Fee -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider @@ -17,44 +14,34 @@ import kotlinx.collections.immutable.persistentListOf internal class SendFeeCustomFieldConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, ) : Converter> { - override fun convert(value: Fee): ImmutableList { - val ethereumFee = value as? Fee.Ethereum ?: return persistentListOf() - val appCurrency = appCurrencyProvider() - - val maxFeeFiat = BigDecimalFormatter.formatFiatAmount( - fiatAmount = ethereumFee.amount.value, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - - return persistentListOf( - SendTextField.CustomFee( - value = ethereumFee.amount.value.toString(), - onValueChange = { clickIntents.onCustomFeeValueChange(0, it) }, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Number, - ), - label = TextReference.Str(maxFeeFiat), - ), - SendTextField.CustomFee( - value = ethereumFee.gasPrice.toString(), - onValueChange = { clickIntents.onCustomFeeValueChange(1, it) }, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Number, - ), - ), - SendTextField.CustomFee( - value = ethereumFee.gasLimit.toString(), - onValueChange = { clickIntents.onCustomFeeValueChange(2, it) }, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Number, - ), - ), + private val ethereumCustomFeeConverter by lazy { + EthereumCustomFeeConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } + + override fun convert(value: Fee): ImmutableList { + return when (value) { + is Fee.Ethereum -> { + ethereumCustomFeeConverter.convert(value) + } + else -> persistentListOf() + } + } + + fun onValueChange(feeSelectorState: FeeSelectorState.Content, index: Int, value: String) = feeSelectorState.copy( + customValues = when (feeSelectorState.fees.normal) { + is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange( + customValues = feeSelectorState.customValues, + index = index, + value = value, + ) + else -> feeSelectorState.customValues + }, + ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt index 8763cf5cc2..d58cb3cd98 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt @@ -1,13 +1,11 @@ package com.tangem.features.send.impl.presentation.state.fee -import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.send.impl.R -@Immutable sealed class SendFeeNotification(val config: NotificationConfig) { sealed class Informational( @@ -29,11 +27,13 @@ sealed class SendFeeNotification(val config: NotificationConfig) { sealed class Warning( val title: TextReference, val subtitle: TextReference, + val buttonsState: NotificationConfig.ButtonsState? = null, ) : SendFeeNotification( config = NotificationConfig( title = title, subtitle = subtitle, iconResId = R.drawable.img_attention_20, + buttonsState = buttonsState, ), ) { data class TooHigh( @@ -47,13 +47,22 @@ sealed class SendFeeNotification(val config: NotificationConfig) { title = resourceReference(id = R.string.send_network_fee_warning_title), subtitle = resourceReference(id = R.string.send_network_fee_warning_content), ) + + data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference(R.string.send_fee_unreachable_error_text), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = onRefresh, + ), + ) } sealed class Error( val title: TextReference, val subtitle: TextReference, val iconResId: Int, - val buttonsState: NotificationConfig.ButtonsState, + val buttonsState: NotificationConfig.ButtonsState? = null, ) : SendFeeNotification( config = NotificationConfig( title = title, @@ -64,15 +73,27 @@ sealed class SendFeeNotification(val config: NotificationConfig) { ) { data class ExceedsBalance( val networkIconId: Int, - val onClick: () -> Unit, + val currencyName: String, + val feeName: String, + val feeSymbol: String, + val networkName: String, + val onClick: (() -> Unit)? = null, ) : Error( - title = resourceReference(id = R.string.send_notification_exceed_fee_title), - subtitle = resourceReference(id = R.string.send_notification_exceed_fee_text), - iconResId = networkIconId, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.common_go_to_provider), - onClick = onClick, + title = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_title, + wrappedList(feeName), ), + subtitle = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_message, + formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol), + ), + iconResId = networkIconId, + buttonsState = onClick?.let { + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_buy_currency, wrappedList(feeName)), + onClick = onClick, + ) + }, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index d87f1a2795..1dbee6b3fd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -1,17 +1,32 @@ package com.tangem.features.send.impl.presentation.state.fee +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal internal class SendFeeStateConverter( + private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, ) : Converter { override fun convert(value: Unit): SendStates.FeeState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() return SendStates.FeeState( - cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + feeSelectorState = FeeSelectorState.Loading, + isSubtractAvailable = false, + isSubtract = false, + isUserSubtracted = false, + fee = null, + receivedAmountValue = BigDecimal.ZERO, + receivedAmount = "", + notifications = persistentListOf(), + rate = cryptoCurrencyStatus.value.fiatRate, + appCurrency = appCurrencyProvider(), + isFeeApproximate = false, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt new file mode 100644 index 0000000000..4997139e4e --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -0,0 +1,159 @@ +package com.tangem.features.send.impl.presentation.state.fee.custom + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal +import java.math.RoundingMode + +internal class EthereumCustomFeeConverter( + private val clickIntents: SendClickIntents, + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter> { + + override fun convert(value: Fee.Ethereum): ImmutableList { + return persistentListOf( + SendTextField.CustomFee( + value = value.amount.value?.parseBigDecimal(value.amount.decimals).orEmpty(), + decimals = value.amount.decimals, + symbol = value.amount.currencySymbol, + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + title = resourceReference(R.string.send_max_fee), + footer = resourceReference(R.string.send_max_fee_footer), + label = getFeeFormatted(value.amount.value), + ), + SendTextField.CustomFee( + value = value.gasPrice.toString(), + decimals = ETHEREUM_GAS_DECIMALS, + symbol = ETHEREUM_GAS_UNIT, + title = resourceReference(R.string.send_gas_price), + footer = resourceReference(R.string.send_gas_price_footer), + onValueChange = { clickIntents.onCustomFeeValueChange(GAS_PRICE, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + ), + SendTextField.CustomFee( + value = value.gasLimit.toString(), + decimals = GAS_DECIMALS, + symbol = null, + title = resourceReference(R.string.send_gas_limit), + footer = resourceReference(R.string.send_gas_limit_footer), + onValueChange = { clickIntents.onCustomFeeValueChange(GAS_LIMIT, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + ), + ) + } + + fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList): Fee.Ethereum { + val feeAmount = value[FEE_AMOUNT].value.parseToBigDecimal(value[FEE_AMOUNT].decimals) + val gasPrice = value[GAS_PRICE].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger() + val gasLimit = value[GAS_LIMIT].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger() + return normalFee.copy( + amount = normalFee.amount.copy(value = feeAmount), + gasPrice = gasPrice, + gasLimit = gasLimit, + ) + } + + fun onValueChange( + customValues: ImmutableList, + index: Int, + value: String, + ): ImmutableList { + val mutableCustomValues = customValues.toMutableList() + return mutableCustomValues.apply { + val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals) + when (index) { + FEE_AMOUNT -> { + val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals) + val newFeeAmount = newFeeAmountDecimal.movePointRight(this[FEE_AMOUNT].decimals) + val newGasPrice = newFeeAmount.divide(gasLimit, GAS_DECIMALS, RoundingMode.HALF_UP) + set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(GAS_DECIMALS))) + set( + index, + this[index].copy( + value = value, + label = getFeeFormatted(newFeeAmountDecimal), + ), + ) + } + GAS_PRICE -> { + val newGasPrice = value.parseToBigDecimal(this[GAS_PRICE].decimals) + .movePointLeft(this[GAS_PRICE].decimals) + val newFeeAmount = gasLimit * newGasPrice + set( + FEE_AMOUNT, + this[FEE_AMOUNT].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), + label = getFeeFormatted(newFeeAmount), + ), + ) + set(index, this[index].copy(value = value)) + } + else -> { + val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals) + val gasPrice = this[GAS_PRICE].value.parseToBigDecimal(this[GAS_PRICE].decimals) + .movePointLeft(this[FEE_AMOUNT].decimals) + val newFeeAmount = newGasLimit * gasPrice + set( + FEE_AMOUNT, + this[FEE_AMOUNT].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), + label = getFeeFormatted(newFeeAmount), + ), + ) + set(index, this[index].copy(value = value)) + } + } + }.toImmutableList() + } + + private fun getFeeFormatted(fee: BigDecimal?): TextReference { + val appCurrency = appCurrencyProvider() + val rate = cryptoCurrencyStatusProvider().value.fiatRate + val fiatFee = rate?.let { fee?.multiply(it) } + return stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatFee, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ) + } + + companion object { + private const val ETHEREUM_GAS_UNIT = "GWEI" + private const val ETHEREUM_GAS_DECIMALS = 18 + private const val FEE_AMOUNT = 0 + private const val GAS_PRICE = 1 + private const val GAS_LIMIT = 2 + private const val GAS_DECIMALS = 0 + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index 66f31d9233..7a6c696c1a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -1,55 +1,48 @@ package com.tangem.features.send.impl.presentation.state.fields +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import java.math.BigDecimal -import java.text.NumberFormat +import java.math.RoundingMode internal class SendAmountFieldChangeConverter( private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, ) : Converter { override fun convert(value: String): SendUiState { val state = currentStateProvider() val amountState = state.amountState ?: return state + val amountTextField = amountState.amountTextField val feeState = state.feeState ?: return state - if (value.checkDecimalSeparatorDuplicate()) return state if (value.isEmpty()) return state.emptyState() - val fiatRate = amountState.cryptoCurrencyStatus.value.fiatRate + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + val trimmedValue = value.trim() - val cryptoValue = if (amountState.isFiatValue) { - if (value.isNotBlank()) { - trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() - } else { - DEFAULT_VALUE - } - } else { - trimmedValue - } + val cryptoValue = trimmedValue.getCryptoValue(amountTextField.isFiatValue, cryptoDecimals) + val fiatValue = trimmedValue.getFiatValue(amountTextField.isFiatValue, fiatDecimals) + val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals) + val decimalFiatValue = fiatValue.parseToBigDecimal(fiatDecimals) - val fiatValue = if (!amountState.isFiatValue) { - if (value.isNotBlank()) { - trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() - } else { - DEFAULT_VALUE - } - } else { - trimmedValue - } - - val isExceedBalance = cryptoValue.checkExceedBalance(amountState) - val isMaxAmount = cryptoValue.checkMaxAmount(amountState) + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue + val isExceedBalance = checkValue.checkExceedBalance(amountTextField) + val isMaxAmount = checkValue.checkMaxAmount(amountTextField) return state.copy( amountState = amountState.copy( isPrimaryButtonEnabled = !isExceedBalance, - amountTextField = amountState.amountTextField.copy( + amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, isError = isExceedBalance, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), ), ), feeState = feeState.copy( @@ -58,58 +51,63 @@ internal class SendAmountFieldChangeConverter( ) } + private fun String.getCryptoValue(isFiatValue: Boolean, decimals: Int): String { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val fiatRate = cryptoCurrencyStatus.value.fiatRate + return if (isFiatValue && fiatRate != null) { + parseToBigDecimal(decimals).divide(fiatRate, decimals, RoundingMode.DOWN) + .parseBigDecimal(decimals) + } else { + this + } + } + + private fun String.getFiatValue(isFiatValue: Boolean, decimals: Int): String { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val fiatRate = cryptoCurrencyStatus.value.fiatRate + return if (!isFiatValue && fiatRate != null) { + parseToBigDecimal(decimals).multiply(fiatRate).parseBigDecimal(decimals) + } else { + this + } + } + private fun SendUiState.emptyState(): SendUiState { return copy( amountState = amountState?.copy( isPrimaryButtonEnabled = false, amountTextField = amountState.amountTextField.copy( - value = if (!amountState.isFiatValue) "" else DEFAULT_VALUE, - fiatValue = if (amountState.isFiatValue) "" else DEFAULT_VALUE, + value = "", + fiatValue = "", isError = false, ), ), ) } - private fun String.checkDecimalSeparatorDuplicate(): Boolean { - val regex = TRIM_REGEX.toRegex() - val decimalSeparatorCount = regex.findAll(this).count() - - return decimalSeparatorCount > 1 - } - - private fun String.checkExceedBalance(state: SendStates.AmountState): Boolean { - val currencyCryptoAmount = state.cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO - val currencyFiatAmount = state.cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO - return if (state.isFiatValue) { - toBigDecimal() > currencyFiatAmount + private fun String.checkExceedBalance(amountTextField: SendTextField.AmountField): Boolean { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO + return if (amountTextField.isFiatValue) { + parseToBigDecimal(amountTextField.fiatAmount.decimals) > currencyFiatAmount } else { - toBigDecimal() > currencyCryptoAmount + parseToBigDecimal(amountTextField.cryptoAmount.decimals) > currencyCryptoAmount } } - private fun String.checkMaxAmount(state: SendStates.AmountState): Boolean { + private fun String.checkMaxAmount(amountTextField: SendTextField.AmountField): Boolean { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + // If current currency is Token - if (state.cryptoCurrencyStatus.currency is CryptoCurrency.Token) return false + if (cryptoCurrencyStatus.currency is CryptoCurrency.Token) return false - val currencyCryptoAmount = state.cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO - val currencyFiatAmount = state.cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO - return if (state.isFiatValue) { - toBigDecimal() == currencyFiatAmount + val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO + return if (amountTextField.isFiatValue) { + parseToBigDecimal(amountTextField.fiatAmount.decimals) == currencyFiatAmount } else { - toBigDecimal() == currencyCryptoAmount + parseToBigDecimal(amountTextField.cryptoAmount.decimals) == currencyCryptoAmount } } - - private fun String.trim(): String { - var trimmedValue = this - if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1) - - return trimmedValue.replace(TRIM_REGEX.toRegex(), ".") - } - - companion object { - private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) - private const val TRIM_REGEX = "[.,]" - } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt index 9ab26bd83d..25a63f78f8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt @@ -4,31 +4,47 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.Amount +import com.tangem.domain.tokens.model.AmountType +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.convertToAmount import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import java.text.NumberFormat +import java.math.BigDecimal + +private const val FIAT_DECIMALS = 2 internal class SendAmountFieldConverter( private val clickIntents: SendClickIntents, -) : Converter { + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, +) : Converter { - override fun convert(value: Unit): SendTextField.Amount { - return SendTextField.Amount( + override fun convert(value: Unit): SendTextField.AmountField { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + return SendTextField.AmountField( value = "", - fiatValue = DEFAULT_VALUE, + fiatValue = "", onValueChange = clickIntents::onAmountValueChange, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next, keyboardType = KeyboardType.Number, ), - placeholder = TextReference.Str(DEFAULT_VALUE), + isFiatValue = false, + cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrencyStatus.currency), + fiatAmount = getAppCurrencyAmount(appCurrencyProvider()), isError = false, error = TextReference.Res(R.string.swapping_insufficient_funds), ) } - companion object { - private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) - } + private fun getAppCurrencyAmount(appCurrency: AppCurrency) = Amount( + currencySymbol = appCurrency.symbol, + value = BigDecimal.ZERO, + decimals = FIAT_DECIMALS, + type = AmountType.FiatType(appCurrency.code), + ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index 456359a16c..42b2ac5b0d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state.fields import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.Amount @Immutable internal sealed class SendTextField { @@ -16,14 +17,13 @@ internal sealed class SendTextField { /** Keyboard options */ abstract val keyboardOptions: KeyboardOptions - // /** Placeholder (hint) */ - // abstract val placeholder: TextReference - - data class Amount( + data class AmountField( override val value: String, override val onValueChange: (String) -> Unit, override val keyboardOptions: KeyboardOptions, - val placeholder: TextReference, + val cryptoAmount: Amount, + val fiatAmount: Amount, + val isFiatValue: Boolean, val fiatValue: String, val isError: Boolean, val error: TextReference, @@ -53,6 +53,10 @@ internal sealed class SendTextField { override val value: String, override val onValueChange: (String) -> Unit, override val keyboardOptions: KeyboardOptions, + val symbol: String?, + val decimals: Int, + val title: TextReference, + val footer: TextReference, val label: TextReference? = null, ) : SendTextField() } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt new file mode 100644 index 0000000000..84e638eb8a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt @@ -0,0 +1,73 @@ +package com.tangem.features.send.impl.presentation.ui + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendAlertState +import com.tangem.features.send.impl.presentation.state.SendEvent + +@Composable +internal fun SendEventEffect(event: StateEvent, snackbarHostState: SnackbarHostState) { + val resources = LocalContext.current.resources + var alertConfig by remember { mutableStateOf(value = null) } + + alertConfig?.let { + SendAlert(state = it, onDismiss = { alertConfig = null }) + } + + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is SendEvent.ShowSnackBar -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + is SendEvent.ShowAlert -> { + alertConfig = value.alert + } + } + }, + ) +} + +@Composable +internal fun SendAlert(state: SendAlertState, onDismiss: () -> Unit) { + val confirmButton: DialogButton + val dismissButton: DialogButton? + + val onActionClick = state.onConfirmClick + if (onActionClick != null) { + confirmButton = DialogButton( + title = state.confirmButtonText.resolveReference(), + onClick = { + onActionClick() + onDismiss() + }, + ) + dismissButton = DialogButton( + title = stringResource(id = R.string.common_cancel), + onClick = onDismiss, + ) + } else { + confirmButton = DialogButton( + title = state.confirmButtonText.resolveReference(), + onClick = onDismiss, + ) + dismissButton = null + } + + BasicDialog( + message = state.message.resolveReference(), + confirmButton = confirmButton, + onDismissDialog = onDismiss, + title = state.title?.resolveReference(), + dismissButton = dismissButton, + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index eae5388921..8fe5c5f53d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -7,8 +7,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -28,6 +30,7 @@ import com.tangem.features.send.impl.presentation.ui.send.SendContent internal fun SendScreen(uiState: SendUiState) { val currentState = uiState.currentState.collectAsStateWithLifecycle() val isSuccess = uiState.sendState.isSuccess + val snackbarHostState = remember { SnackbarHostState() } BackHandler { uiState.clickIntents.onBackClick() } Column( modifier = Modifier @@ -65,6 +68,11 @@ internal fun SendScreen(uiState: SendUiState) { ) SendNavigationButtons(uiState) } + + SendEventEffect( + event = uiState.event, + snackbarHostState = snackbarHostState, + ) } @Composable diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index f6471c020d..c2ca7a26d8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -3,48 +3,49 @@ package com.tangem.features.send.impl.presentation.ui.amount import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.BottomCenter -import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.style.TextAlign -import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.defaultFormat +import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.send.impl.presentation.state.fields.SendTextField @Composable -internal fun ColumnScope.AmountField( - sendField: SendTextField.Amount, - cryptoSymbol: String, - fiatSymbol: String, - isFiat: Boolean, -) { - val value = if (isFiat) sendField.fiatValue else sendField.value - val secondaryValue = if (!isFiat) sendField.fiatValue else sendField.value - val symbol = if (isFiat) fiatSymbol else cryptoSymbol - val secondarySymbol = if (!isFiat) fiatSymbol else cryptoSymbol +internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean) { + val decimalFormat = rememberDecimalFormat() + val (primaryValue, secondaryValue) = if (isFiat) { + sendField.fiatValue to sendField.value + } else { + sendField.value to sendField.fiatValue + } - AmountFieldInner( - value = value, - placeholder = sendField.placeholder, - symbol = symbol, + val (primaryAmount, secondaryAmount) = if (!isFiat) { + sendField.cryptoAmount to sendField.fiatAmount + } else { + sendField.fiatAmount to sendField.cryptoAmount + } + + AmountTextField( + value = primaryValue, + decimals = primaryAmount.decimals, + symbol = primaryAmount.currencySymbol, onValueChange = sendField.onValueChange, keyboardOptions = sendField.keyboardOptions, + textStyle = TangemTheme.typography.h2.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + placeholderAlignment = TopCenter, modifier = Modifier - .align(CenterHorizontally) .padding( top = TangemTheme.dimens.spacing24, start = TangemTheme.dimens.spacing12, @@ -54,15 +55,15 @@ internal fun ColumnScope.AmountField( Box( modifier = Modifier - .align(CenterHorizontally) .padding( top = TangemTheme.dimens.spacing8, start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ), ) { + val text = "${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}" Text( - text = "$secondaryValue $secondarySymbol", + text = text, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, @@ -80,47 +81,6 @@ internal fun ColumnScope.AmountField( } } -@Composable -private fun AmountFieldInner( - value: String, - placeholder: TextReference, - symbol: String, - onValueChange: (String) -> Unit, - keyboardOptions: KeyboardOptions, - modifier: Modifier = Modifier, -) { - val focusRequester = remember { FocusRequester() } - BasicTextField( - value = value, - onValueChange = onValueChange, - modifier = modifier - .focusRequester(focusRequester) - .background(TangemTheme.colors.background.action), - textStyle = TangemTheme.typography.h2.copy( - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ), - keyboardOptions = keyboardOptions, - singleLine = true, - visualTransformation = AmountVisualTransformation(symbol), - decorationBox = { innerTextField -> - Box { - if (value.isBlank()) { - Text( - text = "${placeholder.resolveReference()} $symbol", - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.disabled, - textAlign = TextAlign.Center, - modifier = Modifier - .align(Alignment.TopCenter), - ) - } - innerTextField() - } - }, - ) -} - @Composable private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) { AnimatedVisibility( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt index 4f95280719..afd7b30b6e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt @@ -12,12 +12,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates @Composable internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: Modifier = Modifier) { Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier .fillMaxWidth() .padding( @@ -33,29 +35,24 @@ internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing14) - .align(Alignment.CenterHorizontally), + .padding(top = TangemTheme.dimens.spacing14), ) Text( - text = amountState.walletBalance, + text = amountState.walletBalance.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing2) - .align(Alignment.CenterHorizontally), + .padding(top = TangemTheme.dimens.spacing2), ) TokenIcon( state = amountState.tokenIconState, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing32) - .align(Alignment.CenterHorizontally), + .padding(top = TangemTheme.dimens.spacing32), ) AmountField( sendField = amountState.amountTextField, - isFiat = amountState.isFiatValue, - cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol, - fiatSymbol = amountState.appCurrency.symbol, + isFiat = amountState.amountTextField.isFiatValue, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt index 69bc85a649..a5eba38a01 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt @@ -5,86 +5,64 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation -import com.tangem.core.ui.components.inputrow.InputRowEnter -import com.tangem.core.ui.components.inputrow.InputRowEnterInfo -import com.tangem.core.ui.extensions.TextReference +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.R import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import kotlinx.collections.immutable.ImmutableList -private const val ETHEREUM_UNIT = "GWEI" - @Composable internal fun SendCustomFeeEthereum( customValues: ImmutableList, selectedFee: FeeType, - symbol: String, modifier: Modifier = Modifier, ) { if (selectedFee == FeeType.CUSTOM && customValues.isNotEmpty()) { - val fee = customValues[0] - val gasPrice = customValues[1] - val gasLimit = customValues[2] - Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = modifier, ) { - FooterContainer( - footer = stringResource(R.string.send_max_fee_footer), - ) { - InputRowEnterInfo( - text = fee.value, - title = TextReference.Res(R.string.send_max_fee), - info = fee.label, - visualTransformation = AmountVisualTransformation(symbol), - keyboardOptions = fee.keyboardOptions, - onValueChange = fee.onValueChange, - isSingleLine = true, - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) - } - FooterContainer( - footer = stringResource(R.string.send_gas_price_footer), - ) { - InputRowEnter( - text = gasPrice.value, - title = TextReference.Res(R.string.send_gas_price), - onValueChange = gasPrice.onValueChange, - visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT), - keyboardOptions = fee.keyboardOptions, - isSingleLine = true, - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) - } - FooterContainer( - footer = stringResource(R.string.send_gas_limit_footer), - ) { - InputRowEnter( - text = gasLimit.value, - title = TextReference.Res(R.string.send_gas_limit), - onValueChange = gasLimit.onValueChange, - keyboardOptions = fee.keyboardOptions, - isSingleLine = true, - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) + repeat(customValues.size) { index -> + val value = customValues[index] + FooterContainer( + footer = value.footer.resolveReference(), + ) { + if (value.label != null) { + InputRowEnterInfoAmount( + text = value.value, + decimals = value.decimals, + symbol = value.symbol, + title = value.title, + info = value.label, + keyboardOptions = value.keyboardOptions, + onValueChange = value.onValueChange, + showDivider = false, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } else { + InputRowEnterAmount( + text = value.value, + decimals = value.decimals, + title = value.title, + symbol = value.symbol, + onValueChange = value.onValueChange, + keyboardOptions = value.keyboardOptions, + showDivider = false, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + } } } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index 4c8c06116d..b253efe2e5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -3,7 +3,6 @@ package com.tangem.features.send.impl.presentation.ui.fee import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -16,7 +15,6 @@ import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState -import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fee.SendFeeNotification import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList @@ -24,6 +22,7 @@ import kotlinx.collections.immutable.ImmutableList private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY" private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY" +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) { if (state == null) return @@ -36,25 +35,22 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S .padding( horizontal = TangemTheme.dimens.spacing16, ), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { item( key = FEE_SELECTOR_KEY, ) { SendSpeedSelector( - state = feeSendState, + state = state, clickIntents = clickIntents, + modifier = Modifier.animateItemPlacement(), ) } + customFee(feeSendState) notifications(notifications) - customFee( - feeSendState = feeSendState, - cryptoCurrencySymbol = state.cryptoCurrencyStatus.currency.symbol, - ) subtractButton( - feeSendState = feeSendState, receivedAmount = state.receivedAmount, isSubtract = state.isSubtract, + isSubtractAvailable = state.isSubtractAvailable, clickIntents = clickIntents, ) } @@ -69,10 +65,24 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.background.primary + else -> TangemTheme.colors.button.disabled + }, iconTint = when (it) { is SendFeeNotification.Informational -> TangemTheme.colors.icon.accent + is SendFeeNotification.Error.ExceedsBalance -> { + if (it.config.buttonsState == null) { + TangemTheme.colors.icon.warning + } else { + null + } + } else -> null }, ) @@ -81,11 +91,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList + if (isSubtractAvailable) { item { - val selectedFeeValue = state.selectedFee - val topPadding = if (selectedFeeValue != FeeType.CUSTOM) { - TangemTheme.dimens.spacing8 - } else { - TangemTheme.dimens.spacing0 - } - SendSpeedSubtract( receivingAmount = receivedAmount, isSubtract = isSubtract, onSelectClick = clickIntents::onSubtractSelect, modifier = modifier - .animateItemPlacement() - .padding( - top = topPadding, - bottom = TangemTheme.dimens.spacing12, - ), + .padding(vertical = TangemTheme.dimens.spacing12) + .animateItemPlacement(), ) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index f221817a1d..db73ae5c7e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.ui.fee import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -13,29 +14,45 @@ import androidx.compose.runtime.getValue 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.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference 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.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.BigDecimalFormatter.CAN_BE_LOWER_SIGN +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import java.math.BigDecimal + +private val DEFAULT_FEE_OPTIONS = listOf( + R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24, + R.string.common_fee_selector_option_market to R.drawable.ic_bird_24, + R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24, +) @Suppress("LongMethod") @Composable internal fun SendSpeedSelector( - state: FeeSelectorState, + state: SendStates.FeeState, clickIntents: SendClickIntents, modifier: Modifier = Modifier, ) { @@ -49,58 +66,70 @@ internal fun SendSpeedSelector( .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), ) { - when (state) { + when (val feeSelectorState = state.feeSelectorState) { + FeeSelectorState.Error -> { + SendSpeedSelectorItemError() + } FeeSelectorState.Loading -> { SendSpeedSelectorItemLoading() - SendSpeedSelectorItemLoading() - SendSpeedSelectorItemLoading() } is FeeSelectorState.Content -> { - when (state.fees) { + when (val fees = feeSelectorState.fees) { is TransactionFee.Choosable -> { - val isSelected = state.selectedFee + val isSelected = feeSelectorState.selectedFee + val minimumAmount = fees.minimum.amount SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_slow, iconRes = R.drawable.ic_tortoise_24, - amount = TextReference.Str(state.fees.minimum.amount.value.toString()), - symbol = TextReference.Str(state.fees.minimum.amount.currencySymbol), + amount = getCryptoReference(minimumAmount, state.isFeeApproximate), + fiatAmount = getFiatReference(minimumAmount, state.rate, state.appCurrency), + symbolLength = minimumAmount.currencySymbol.length, isSelected = isSelected == FeeType.SLOW, onSelect = { clickIntents.onFeeSelectorClick(FeeType.SLOW) }, ) + val normalAmount = fees.normal.amount SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, - amount = TextReference.Str(state.fees.normal.amount.value.toString()), - symbol = TextReference.Str(state.fees.normal.amount.currencySymbol), + amount = getCryptoReference(normalAmount, state.isFeeApproximate), + fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency), + symbolLength = normalAmount.currencySymbol.length, isSelected = isSelected == FeeType.MARKET, onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) }, ) + val priorityAmount = fees.priority.amount SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_fast, iconRes = R.drawable.ic_hare_24, - amount = TextReference.Str(state.fees.priority.amount.value.toString()), - symbol = TextReference.Str(state.fees.priority.amount.currencySymbol), + amount = getCryptoReference(priorityAmount, state.isFeeApproximate), + fiatAmount = getFiatReference(priorityAmount, state.rate, state.appCurrency), + symbolLength = priorityAmount.currencySymbol.length, isSelected = isSelected == FeeType.FAST, onSelect = { clickIntents.onFeeSelectorClick(FeeType.FAST) }, - showDivider = state.fees.normal is Fee.Ethereum, + showDivider = fees.normal is Fee.Ethereum, ) - if (state.fees.normal is Fee.Ethereum) { + AnimatedVisibility( + visible = fees.normal is Fee.Ethereum, + label = "Custom fee appearance animation", + ) { SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_custom, iconRes = R.drawable.ic_edit_24, isSelected = isSelected == FeeType.CUSTOM, onSelect = { clickIntents.onFeeSelectorClick(FeeType.CUSTOM) }, - showDivider = state.fees.normal !is Fee.Ethereum, + showDivider = fees.normal !is Fee.Ethereum, ) } } is TransactionFee.Single -> { + val normalAmount = feeSelectorState.fees.normal.amount SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, isSelected = true, - amount = TextReference.Str(state.fees.normal.amount.value.toString()), - symbol = TextReference.Str(state.fees.normal.amount.currencySymbol), + amount = getCryptoReference(normalAmount, state.isFeeApproximate), + fiatAmount = getFiatReference(normalAmount, state.rate, state.appCurrency), + symbolLength = normalAmount.currencySymbol.length, onSelect = { clickIntents.onFeeSelectorClick(FeeType.MARKET) }, showDivider = false, ) @@ -112,36 +141,75 @@ internal fun SendSpeedSelector( } } +// todo remove after refactoring [REDACTED_JIRA] +private fun getCryptoReference(amount: Amount, isFeeApproximate: Boolean) = combinedReference( + if (isFeeApproximate) stringReference("$CAN_BE_LOWER_SIGN ") else TextReference.EMPTY, + stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = amount.value, + cryptoCurrency = amount.currencySymbol, + decimals = amount.decimals, + ), + ), +) + +// todo remove after refactoring [REDACTED_JIRA] +private fun getFiatReference(amount: Amount, rate: BigDecimal?, appCurrency: AppCurrency) = stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = rate?.let { amount.value?.multiply(it) }, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), +) + @Composable private fun SendSpeedSelectorItemLoading() { - Row(modifier = Modifier.fillMaxWidth()) { - RectangleShimmer( - radius = TangemTheme.dimens.radius3, - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing18, - bottom = TangemTheme.dimens.spacing18, - start = TangemTheme.dimens.spacing12, - ) - .size( - width = TangemTheme.dimens.size50, - height = TangemTheme.dimens.size12, - ), - ) - SpacerWMax() - RectangleShimmer( - radius = TangemTheme.dimens.radius3, - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing18, - bottom = TangemTheme.dimens.spacing18, - end = TangemTheme.dimens.spacing12, - ) - .size( - width = TangemTheme.dimens.size90, - height = TangemTheme.dimens.size12, - ), - ) + repeat(DEFAULT_FEE_OPTIONS.size) { + val (text, iconRes) = DEFAULT_FEE_OPTIONS[it] + Row(modifier = Modifier.fillMaxWidth()) { + SelectorTitleContent( + titleRes = text, + iconRes = iconRes, + ) + SpacerWMax() + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing18, + bottom = TangemTheme.dimens.spacing18, + end = TangemTheme.dimens.spacing12, + ) + .size( + width = TangemTheme.dimens.size90, + height = TangemTheme.dimens.size12, + ), + ) + } + } +} + +@Composable +private fun SendSpeedSelectorItemError() { + repeat(DEFAULT_FEE_OPTIONS.size) { + val (text, iconRes) = DEFAULT_FEE_OPTIONS[it] + Row(modifier = Modifier.fillMaxWidth()) { + SelectorTitleContent( + titleRes = text, + iconRes = iconRes, + ) + SpacerWMax() + Text( + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + vertical = TangemTheme.dimens.spacing14, + horizontal = TangemTheme.dimens.spacing12, + ), + ) + } } } @@ -152,7 +220,8 @@ private fun SendSpeedSelectorItem( onSelect: () -> Unit, modifier: Modifier = Modifier, amount: TextReference? = null, - symbol: TextReference? = null, + fiatAmount: TextReference? = null, + symbolLength: Int? = null, isSelected: Boolean = false, showDivider: Boolean = true, ) { @@ -177,32 +246,17 @@ private fun SendSpeedSelectorItem( .clickable { onSelect() }, ) { Row(modifier = Modifier.fillMaxWidth()) { - Icon( - painter = painterResource(iconRes), - tint = iconTint, - contentDescription = null, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), + SelectorTitleContent( + titleRes = titleRes, + iconRes = iconRes, + iconTint = iconTint, + textStyle = textStyle, ) - Text( - text = stringResource(titleRes), - style = textStyle, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing8, - top = TangemTheme.dimens.spacing14, - bottom = TangemTheme.dimens.spacing14, - ), - ) - if (amount != null && symbol != null) { + if (amount != null && symbolLength != null && fiatAmount != null) { SelectorValueContent( amount = amount, - symbol = symbol, + fiatAmount = fiatAmount, + symbolLength = symbolLength, textStyle = textStyle, ) } @@ -221,14 +275,49 @@ private fun SendSpeedSelectorItem( } @Composable -private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) { +private fun SelectorTitleContent( + @StringRes titleRes: Int, + @DrawableRes iconRes: Int, + iconTint: Color = TangemTheme.colors.icon.informative, + textStyle: TextStyle = TangemTheme.typography.body2, +) { + Icon( + painter = painterResource(iconRes), + tint = iconTint, + contentDescription = null, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) Text( + text = stringResource(titleRes), + style = textStyle, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing8, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) +} + +@Composable +private fun RowScope.SelectorValueContent( + amount: TextReference, + fiatAmount: TextReference, + symbolLength: Int, + textStyle: TextStyle, +) { + EllipsisText( text = amount.resolveReference(), style = textStyle, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.End, - overflow = TextOverflow.Ellipsis, - maxLines = 1, + ellipsis = TextEllipsis.OffsetEnd(symbolLength), modifier = Modifier .weight(1f) .padding( @@ -238,12 +327,12 @@ private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextRef ), ) Text( - text = symbol.resolveReference(), + text = "(${fiatAmount.resolveReference()})", style = textStyle, color = TangemTheme.colors.text.primary1, modifier = Modifier .padding( - start = TangemTheme.dimens.spacing1, + start = TangemTheme.dimens.spacing4, end = TangemTheme.dimens.spacing12, top = TangemTheme.dimens.spacing14, bottom = TangemTheme.dimens.spacing14, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt index e83ba769bd..d16846ae48 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt @@ -24,6 +24,7 @@ import androidx.constraintlayout.compose.Visibility import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R @@ -49,10 +50,11 @@ fun ListItemWithIcon( subtitleEndOffset: Int = 0, @DrawableRes subtitleIconRes: Int? = null, ) { + val hapticFeedback = rememberHapticFeedback(state = title, onAction = onClick) ConstraintLayout( modifier = modifier .fillMaxWidth() - .clickable { onClick() } + .clickable { hapticFeedback() } .padding(horizontal = TangemTheme.dimens.spacing12), ) { val (iconRef, titleRef, subtitleRef, subtitleIconRef) = createRefs() 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 57187dffc1..d2741dffcc 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 @@ -59,7 +59,6 @@ internal fun SendRecipientContent( placeholder = address.placeholder, onValueChange = address.onValueChange, onPasteClick = clickIntents::onRecipientAddressValueChange, - singleLine = true, isError = isError, isLoading = isValidating, error = address.error, 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 8b3c83bfb0..758fef6f57 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 @@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.ui.recipient import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -55,6 +56,7 @@ internal fun TextFieldWithPaste( placeholder = placeholder, onValueChange = onValueChange, modifier = Modifier + .fillMaxWidth() .padding(top = TangemTheme.dimens.spacing6), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index b9f84d7264..d014f71caa 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -17,16 +17,19 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle -import com.tangem.blockchain.extensions.toBigDecimalOrDefault import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImage import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault import com.tangem.core.ui.components.notifications.Notification 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.utils.BigDecimalFormatter import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount +import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount +import com.tangem.domain.tokens.model.AmountType import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates @@ -61,7 +64,7 @@ internal fun SendContent(uiState: SendUiState) { AnimatedVisibility(visible = !isSuccess) { FromWallet( walletName = amountState.walletName, - walletBalance = amountState.walletBalance, + walletBalance = amountState.walletBalance.resolveReference(), ) } AmountBlock( @@ -122,13 +125,14 @@ private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: Boolean, val amount = amountState.amountTextField val cryptoAmount = formatCryptoAmount( - cryptoCurrency = amountState.cryptoCurrencyStatus.currency, - cryptoAmount = amount.value.toBigDecimalOrDefault(), + cryptoAmount = amount.cryptoAmount.value, + cryptoCurrency = amount.cryptoAmount.currencySymbol, + decimals = amount.cryptoAmount.decimals, ) - val fiatAmount = BigDecimalFormatter.formatFiatAmount( - fiatAmount = amount.fiatValue.toBigDecimalOrDefault(), - fiatCurrencyCode = amountState.appCurrency.code, - fiatCurrencySymbol = amountState.appCurrency.symbol, + val fiatAmount = formatFiatAmount( + fiatAmount = amount.fiatAmount.value, + fiatCurrencyCode = (amount.fiatAmount.type as AmountType.FiatType).code, + fiatCurrencySymbol = amount.fiatAmount.currencySymbol, ) InputRowImage( title = TextReference.Res(R.string.send_amount_label), @@ -172,14 +176,22 @@ private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: @Composable private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { val fee = feeState.fee ?: return - val feeValue = formatCryptoAmount( + val feeCryptoValue = formatCryptoAmount( cryptoAmount = fee.amount.value, cryptoCurrency = fee.amount.currencySymbol, decimals = fee.amount.decimals, ) + val feeFiatValue = formatFiatAmount( + fiatAmount = feeState.rate?.let { fee.amount.value?.multiply(it) }, + fiatCurrencyCode = feeState.appCurrency.code, + fiatCurrencySymbol = feeState.appCurrency.symbol, + ) InputRowDefault( - title = TextReference.Res(R.string.common_network_fee_title), - text = TextReference.Str(feeValue), + title = resourceReference(R.string.common_network_fee_title), + text = resourceReference( + id = R.string.send_wallet_balance_format, + wrappedList(feeCryptoValue, feeFiatValue), + ), modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) @@ -197,7 +209,10 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.background.action + else -> TangemTheme.colors.button.disabled + }, iconTint = when (it) { is SendNotification.Error -> TangemTheme.colors.icon.warning is SendNotification.Warning -> null diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index f79bf242b5..cd3c448c3f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.send.impl.presentation.state.fee.FeeType +@Suppress("TooManyFunctions") interface SendClickIntents { fun popBackStack() @@ -16,6 +17,8 @@ interface SendClickIntents { fun onQrCodeScanClick() + fun onFailedTxEmailClick(errorMessage: String) + fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) // region Amount @@ -33,6 +36,8 @@ interface SendClickIntents { // endregion // region Fee + fun feeReload() + fun onFeeSelectorClick(feeType: FeeType) fun onCustomFeeValueChange(index: Int, value: String) @@ -50,5 +55,9 @@ interface SendClickIntents { fun showFee() fun onExploreClick(txUrl: String) + + fun onAmountReduceClick(reducedAmount: String) + + fun onAmountReduceIgnoreClick() // endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 133e865e26..b02ffb58d7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -5,19 +5,26 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import androidx.paging.PagingData +import arrow.core.Either import arrow.core.getOrElse import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.extensions.isZero +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase -import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase +import com.tangem.domain.redux.LegacyAction +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase @@ -31,23 +38,17 @@ import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.* -import com.tangem.features.send.impl.presentation.state.SendNotificationFactory -import com.tangem.features.send.impl.presentation.state.SendStateFactory -import com.tangem.features.send.impl.presentation.state.SendUiState -import com.tangem.features.send.impl.presentation.state.StateRouter +import com.tangem.features.send.impl.presentation.state.fee.FeeNotificationFactory import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory import com.tangem.features.send.impl.presentation.state.fee.FeeType -import com.tangem.features.send.impl.presentation.state.fee.getFee import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates @@ -58,6 +59,7 @@ internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, @@ -70,8 +72,12 @@ internal class SendViewModel @Inject constructor( private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, private val parseSharedAddressUseCase: ParseSharedAddressUseCase, private val walletManagersFacade: WalletManagersFacade, + private val reduxStateHolder: ReduxStateHolder, + private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, + isFeeApproximateUseCase: IsFeeApproximateUseCase, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, + getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -93,23 +99,49 @@ internal class SendViewModel @Inject constructor( userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, validateWalletMemoUseCase = validateWalletMemoUseCase, getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase, ) + private val feeStateFactory = FeeStateFactory( + clickIntents = this, + currentStateProvider = Provider { uiState }, + coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + isFeeApproximateUseCase = isFeeApproximateUseCase, + ) + + private val eventStateFactory = SendEventStateFactory( + clickIntents = this, + currentStateProvider = Provider { uiState }, + feeStateFactory = feeStateFactory, + ) + + private val feeNotificationFactory = FeeNotificationFactory( + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + currentStateProvider = Provider { uiState }, + userWalletProvider = Provider { userWallet }, + clickIntents = this, + getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase, + ) + private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, walletManagersFacade = walletManagersFacade, + clickIntents = this, ) + // todo convert to StateFlow var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) private set private var userWallet: UserWallet by Delegates.notNull() + private var isAmountSubtractAvailable: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() @@ -118,11 +150,14 @@ internal class SendViewModel @Inject constructor( private var feeJobHolder = JobHolder() private var addressValidationJobHolder = JobHolder() private var sendNotificationsJobHolder = JobHolder() + private var feeNotificationsJobHolder = JobHolder() private var qrScannerJobHolder = JobHolder() + private var sendIdleTimer = 0L + override fun onCreate(owner: LifecycleOwner) { subscribeOnCurrencyStatusUpdates(owner) - getFee() + onStateActive() } fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) { @@ -136,10 +171,13 @@ internal class SendViewModel @Inject constructor( getUserWalletUseCase(userWalletId).fold( ifRight = { wallet -> userWallet = wallet + checkIfSubtractAvailable() getCurrenciesStatusUpdates(owner, wallet) }, ifLeft = { - // todo add error handling [[REDACTED_JIRA]] + uiState = eventStateFactory.getGenericErrorState( + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) return@launch }, ) @@ -280,37 +318,16 @@ internal class SendViewModel @Inject constructor( } } - private fun getFee() { - viewModelScope.launch(dispatchers.main) { - uiState.currentState - .filter { it == SendUiStateType.Fee } - .onEach { - val amountState = uiState.amountState ?: return@onEach - val recipientState = uiState.recipientState ?: return@onEach - val amount = amountState.amountTextField.value.toBigDecimal() - - uiState = stateFactory.onFeeOnLoadingState() - getFeeUseCase.invoke( - amount = amount, - destination = recipientState.addressTextField.value, - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - ) - .conflate() - .distinctUntilChanged() - .onEach { maybeFee -> - maybeFee.fold( - ifRight = { - uiState = stateFactory.onFeeOnLoadedState(it) - }, - ifLeft = { - // todo add error handling [[REDACTED_JIRA]] - }, - ) - } - .launchIn(viewModelScope) - }.launchIn(viewModelScope) - }.saveIn(feeJobHolder) + private fun onStateActive() { + uiState.currentState + .onEach { + when (it) { + SendUiStateType.Fee -> loadFee() + SendUiStateType.Send -> sendIdleTimer = System.currentTimeMillis() + else -> Unit + } + } + .launchIn(viewModelScope) } private fun updateNotifications() { @@ -323,6 +340,16 @@ internal class SendViewModel @Inject constructor( .saveIn(sendNotificationsJobHolder) } + private fun updateFeeNotifications() { + feeNotificationFactory.create() + .conflate() + .distinctUntilChanged() + .onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(feeNotificationsJobHolder) + } + // region screen state navigation override fun popBackStack() = stateRouter.popBackStack() override fun onBackClick() = stateRouter.onBackClick() @@ -331,6 +358,10 @@ internal class SendViewModel @Inject constructor( override fun onQrCodeScanClick() = innerRouter.openQrCodeScanner(cryptoCurrency.network.name) + override fun onFailedTxEmailClick(errorMessage: String) { + reduxStateHolder.dispatch(LegacyAction.SendEmailTransactionFailed(errorMessage)) + } + override fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) = innerRouter.openTokenDetails(userWalletId, currency) // endregion @@ -346,12 +377,15 @@ internal class SendViewModel @Inject constructor( override fun onMaxValueClick() { val amountState = uiState.amountState ?: return - val amount = if (amountState.isFiatValue) { - amountState.cryptoCurrencyStatus.value.fiatAmount + val amountTextField = amountState.amountTextField + val (amount, decimals) = if (amountTextField.isFiatValue) { + cryptoCurrencyStatus.value.fiatAmount to amountTextField.fiatAmount.decimals } else { - amountState.cryptoCurrencyStatus.value.amount + cryptoCurrencyStatus.value.amount to amountTextField.cryptoAmount.decimals + } + if (amount != null && !amount.isZero()) { + onAmountValueChange(amount.parseBigDecimal(decimals)) } - onAmountValueChange(amount?.toPlainString() ?: DEFAULT_VALUE) } // endregion @@ -415,16 +449,56 @@ internal class SendViewModel @Inject constructor( // endregion // region fee + override fun feeReload() = loadFee() + override fun onFeeSelectorClick(feeType: FeeType) { - uiState = stateFactory.onFeeSelectedState(feeType) + uiState = feeStateFactory.onFeeSelectedState(feeType) + updateFeeNotifications() } override fun onCustomFeeValueChange(index: Int, value: String) { - uiState = stateFactory.onCustomFeeValueChange(index, value) + uiState = feeStateFactory.onCustomFeeValueChange(index, value) + updateFeeNotifications() } override fun onSubtractSelect(value: Boolean) { - uiState = stateFactory.onSubtractSelect(value) + uiState = feeStateFactory.onSubtractSelect(value) + updateFeeNotifications() + } + + private fun loadFee() { + viewModelScope.launch(dispatchers.main) { + uiState = feeStateFactory.onFeeOnLoadingState() + uiState = callFeeUseCase()?.fold( + ifRight = { fees -> + feeStateFactory.onFeeOnLoadedState(fees, isAmountSubtractAvailable) + }, + ifLeft = { + feeStateFactory.onFeeOnErrorState() + }, + ) ?: feeStateFactory.onFeeOnErrorState() + updateFeeNotifications() + }.saveIn(feeJobHolder) + } + + private suspend fun checkIfSubtractAvailable() { + isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrency).fold( + ifRight = { it }, + ifLeft = { false }, + ) + } + + private suspend fun callFeeUseCase(): Either? { + val amountState = uiState.amountState ?: return null + val recipientState = uiState.recipientState ?: return null + val amount = amountState.amountTextField.cryptoAmount.value ?: return null + + return getFeeUseCase.invoke( + amount = amount, + destination = recipientState.addressTextField.value, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ) } // endregion @@ -433,68 +507,146 @@ internal class SendViewModel @Inject constructor( val sendState = uiState.sendState if (sendState.isSuccess) popBackStack() - uiState = stateFactory.getSendingStateUpdate(true) - viewModelScope.launch(dispatchers.io) { verifyAndSendTransaction() } + uiState = stateFactory.getSendingStateUpdate(isSending = true) + if (System.currentTimeMillis() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) { + verifyAndSendTransaction() + } else { + onCheckFeeUpdate() + } + sendIdleTimer = System.currentTimeMillis() } - override fun showAmount() = stateRouter.showAmount(isFromSend = true) + override fun showAmount() = stateRouter.showAmount() - override fun showRecipient() = stateRouter.showRecipient(isFromSend = true) + override fun showRecipient() = stateRouter.showRecipient() - override fun showFee() = stateRouter.showFee(isFromSend = true) + override fun showFee() = stateRouter.showFee() override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl) - private suspend fun verifyAndSendTransaction() { + override fun onAmountReduceClick(reducedAmount: String) { + uiState = stateFactory.getOnAmountValueChange(reducedAmount) + uiState = sendNotificationFactory.dismissHighFeeWarningState() + loadFee() + } + + override fun onAmountReduceIgnoreClick() { + uiState = sendNotificationFactory.dismissHighFeeWarningState() + } + + private fun verifyAndSendTransaction() { val recipient = uiState.recipientState?.addressTextField?.value ?: return val feeState = uiState.feeState ?: return val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return val memo = uiState.recipientState?.memoTextField?.value - val fee = feeSelectorState.getFee() + val fee = feeStateFactory.feeConverter.convert(feeSelectorState) + val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return + val amountToSend = if (feeState.isSubtract && isAmountSubtractAvailable) { + feeState.receivedAmountValue + } else { + amountValue + } - val amountToSend = feeState.receivedAmountValue.convertToAmount(cryptoCurrency) + viewModelScope.launch(dispatchers.main) { + createTransactionUseCase( + amount = amountToSend.convertToAmount(cryptoCurrency), + fee = fee, + memo = memo, + destination = recipient, + userWalletId = userWalletId, + network = cryptoCurrency.network, + ).fold( + ifLeft = { + Timber.e(it) + uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState = eventStateFactory.getGenericErrorState( + error = it, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + }, + ifRight = { txData -> + sendTransaction(txData) + }, + ) + } + } - // todo add error handling [[REDACTED_JIRA]] - // val transactionErrors = walletManagersFacade.validateTransaction( - // amount = amountToSend, - // fee = fee.amount, - // userWalletId = userWalletId, - // network = cryptoCurrency.network, - // ) - - createTransactionUseCase( - amount = amountToSend, - fee = fee, - memo = memo, - destination = recipient, - userWalletId = userWalletId, + private suspend fun sendTransaction(txData: TransactionData) { + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, network = cryptoCurrency.network, ).fold( - ifLeft = { - Timber.e(it) - // todo add error handling [[REDACTED_JIRA]] - }, - ifRight = { txData -> - sendTransactionUseCase( - txData = txData, - userWallet = userWallet, - network = cryptoCurrency.network, - ).fold( - ifLeft = { - uiState = stateFactory.getSendingStateUpdate(false) - // todo add error handling [[REDACTED_JIRA]] - }, - ifRight = { - uiState = stateFactory.getTransactionSendState(txData) - }, + ifLeft = { error -> + uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState = eventStateFactory.getSendTransactionErrorState( + error = error, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, ) }, + ifRight = { + uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState = stateFactory.getTransactionSendState(txData) + scheduleBalanceUpdate() + }, ) } + + private fun scheduleBalanceUpdate() { + viewModelScope.launch(dispatchers.io) { + delay(BALANCE_UPDATE_DELAY) + fetchCurrencyStatusUseCase.invoke( + userWalletId = userWalletId, + id = cryptoCurrency.id, + refresh = true, + ) + } + } + + private fun onCheckFeeUpdate() { + val isSending = uiState.sendState.isSending + val isSuccess = uiState.sendState.isSuccess + val noErrorNotifications = uiState.sendState.notifications.none { it is SendNotification.Error } + + if (!isSending && !isSuccess && noErrorNotifications) { + viewModelScope.launch(dispatchers.main) { + val feeUpdatedState = callFeeUseCase()?.fold( + ifRight = { + uiState = stateFactory.getSendingStateUpdate(isSending = false) + eventStateFactory.getFeeUpdatedAlert( + fee = it, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onFeeNotIncreased = { + uiState = stateFactory.getSendingStateUpdate(isSending = true) + verifyAndSendTransaction() + }, + ) + }, + ifLeft = { + uiState = stateFactory.getSendingStateUpdate(isSending = false) + eventStateFactory.getGenericErrorState( + error = (it as? GetFeeError.DataError)?.cause, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + }, + ) + + uiState = if (feeUpdatedState != null) { + feeUpdatedState + } else { + uiState = stateFactory.getSendingStateUpdate(isSending = false) + eventStateFactory.getGenericErrorState( + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + } + } + } + } // endregion companion object { private const val XRP_X_ADDRESS = 'X' - private const val DEFAULT_VALUE = "0.00" + private const val CHECK_FEE_UPDATE_DELAY = 60_000L + private const val BALANCE_UPDATE_DELAY = 10_000L } } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index fa28cf44f2..dca890c55e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -11,7 +11,7 @@ internal class ErrorsDataConverter( private val jsonAdapter: JsonAdapter, ) : Converter { - @Suppress("MagicNumber") + @Suppress("MagicNumber", "CyclomaticComplexMethod") override fun convert(value: String): DataError { try { val error = jsonAdapter.fromJson(value)?.error ?: return DataError.UnknownError @@ -23,6 +23,7 @@ internal class ErrorsDataConverter( 2230 -> DataError.ExchangeProviderNotAvailableError(code = error.code) 2240 -> DataError.ExchangeNotPossibleError(code = error.code) 2250 -> tryParseExchangeTooSmallAmountError(error = error) + 2251 -> tryParseExchangeTooBigAmountError(error = error) 2260 -> tryParseExchangeNotEnoughAllowanceError(error = error) 2270 -> DataError.ExchangeNotEnoughBalanceError(code = error.code) 2280 -> DataError.ExchangeInvalidAddressError(code = error.code) @@ -44,6 +45,16 @@ internal class ErrorsDataConverter( ) } + private fun tryParseExchangeTooBigAmountError(error: ExpressError): DataError { + val minAmount = error.value?.maxAmount ?: return DataError.UnknownErrorWithCode(error.code) + val decimals = error.value?.decimals ?: return DataError.UnknownErrorWithCode(error.code) + + return DataError.ExchangeTooBigAmountError( + code = error.code, + amount = createFromAmountWithOffset(minAmount, decimals), + ) + } + private fun tryParseExchangeNotEnoughAllowanceError(error: ExpressError): DataError { val currentAllowance = error.value?.currentAllowance ?: return DataError.UnknownErrorWithCode(error.code) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index 0da33b0bcf..21c40c5588 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -18,6 +18,8 @@ sealed class DataError { data class ExchangeTooSmallAmountError(override val code: Int, val amount: SwapAmount) : DataError() + data class ExchangeTooBigAmountError(override val code: Int, val amount: SwapAmount) : DataError() + data class ExchangeNotEnoughAllowanceError(override val code: Int, val currentAllowance: BigDecimal) : DataError() data class ExchangeNotEnoughBalanceError(override val code: Int) : DataError() diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 08acf5d871..cbf8cd6d81 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -9,6 +9,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder @@ -121,13 +122,13 @@ class SwapDomainModule { @Singleton fun provideSendTransactionUseCase( @SwapScope isDemoCardUseCase: IsDemoCardUseCase, - walletManagersFacade: WalletManagersFacade, cardSdkConfigRepository: CardSdkConfigRepository, + transactionRepository: TransactionRepository, ): SendTransactionUseCase { return SendTransactionUseCase( isDemoCardUseCase = isDemoCardUseCase, cardSdkConfigRepository = cardSdkConfigRepository, - walletManagersFacade = walletManagersFacade, + transactionRepository = transactionRepository, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index bbe757b01b..4e77ff7dc8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -31,6 +31,7 @@ data class SwapStateHolder( val bottomSheetConfig: TangemBottomSheetConfig? = null, val swapButton: SwapButton, + val shouldShowMaxAmount: Boolean, val tosState: TosState? = null, val onRefresh: () -> Unit, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 931563da97..eaddec7e0c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -146,7 +146,7 @@ private fun ProviderContentState( } Row( modifier = Modifier.padding( - top = TangemTheme.dimens.spacing8, + top = TangemTheme.dimens.spacing6, end = TangemTheme.dimens.spacing56, ), ) { @@ -238,7 +238,7 @@ private fun ProviderUnavailableState( text = it.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), ) } } 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 d0d611881d..d6a880f213 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 @@ -87,6 +87,7 @@ internal class StateBuilder( changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, providerState = ProviderState.Empty(), + shouldShowMaxAmount = false, priceImpact = PriceImpact.Empty(), ) } @@ -187,6 +188,7 @@ internal class StateBuilder( permissionState = uiStateHolder.permissionState, changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, priceImpact = PriceImpact.Empty(), + shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toToken), ) } @@ -276,9 +278,14 @@ internal class StateBuilder( PriceImpact.Empty() }, tosState = createTosState(swapProvider), + shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toCurrencyStatus.currency), ) } + private fun shouldShowMaxAmount(fromToken: CryptoCurrency, toCurrency: CryptoCurrency): Boolean { + return !(fromToken is CryptoCurrency.Coin && fromToken.network.id == toCurrency.network.id) + } + private fun createTosState(swapProvider: SwapProvider): TosState { return TosState( tosLink = swapProvider.termsOfUse?.let { @@ -527,6 +534,16 @@ internal class StateBuilder( onProviderClick = onProviderClick, ) } + is DataError.ExchangeTooBigAmountError -> { + swapProvider.convertToAvailableFromProviderState( + alertText = resourceReference( + R.string.express_provider_max_amount, + wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), + ), + selectionType = selectionType, + onProviderClick = onProviderClick, + ) + } else -> { ProviderState.Empty() } @@ -541,7 +558,17 @@ internal class StateBuilder( id = R.string.warning_express_too_minimal_amount_title, formatArgs = wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), ), - subtitle = resourceReference(R.string.warning_express_too_minimal_amount_description), + subtitle = resourceReference(R.string.warning_express_wrong_amount_description), + iconResId = R.drawable.ic_alert_circle_24, + ), + ) + is DataError.ExchangeTooBigAmountError -> SwapWarning.GeneralError( + notificationConfig = NotificationConfig( + title = resourceReference( + id = R.string.warning_express_too_maximum_amount_title, + formatArgs = wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), + ), + subtitle = resourceReference(R.string.warning_express_wrong_amount_description), iconResId = R.drawable.ic_alert_circle_24, ), ) @@ -553,7 +580,7 @@ internal class StateBuilder( resourceReference(R.string.warning_express_refresh_required_title) }, subtitle = if (dataError is DataError.UnknownError) { - resourceReference(R.string.swapping_generic_error) + resourceReference(R.string.common_unknown_error) } else { resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())) }, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index bb85650ace..f58b501863 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -86,7 +86,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi } } - if (keyboard is Keyboard.Opened) { + if (state.shouldShowMaxAmount && keyboard is Keyboard.Opened) { Text( text = stringResource(id = R.string.send_max_amount_label), style = TangemTheme.typography.button, @@ -109,7 +109,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi val message = if (state.alert.type == GenericWarningType.NETWORK) { stringResource(id = R.string.disclaimer_error_loading) } else { - state.alert.message?.resolveReference() ?: stringResource(id = R.string.swapping_generic_error) + state.alert.message?.resolveReference() ?: stringResource(id = R.string.common_unknown_error) } SimpleOkDialog( message = message, @@ -280,7 +280,7 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { Card( elevation = TangemTheme.dimens.elevation3, shape = CircleShape, - backgroundColor = TangemTheme.colors.background.plain, + backgroundColor = TangemTheme.colors.background.action, contentColor = TangemTheme.colors.text.primary1, modifier = modifier.size(TangemTheme.dimens.size48), onClick = state.onChangeCardsClicked, @@ -338,7 +338,7 @@ private fun SwapWarnings(warnings: List) { } else { it.resolveReference() } - } ?: stringResource(id = R.string.swapping_generic_error) + } ?: stringResource(id = R.string.common_unknown_error) RefreshableWaringCard( title = stringResource(id = R.string.common_warning), description = message, @@ -494,6 +494,7 @@ private val state = SwapStateHolder( blockchainId = "POLYGON", providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), + shouldShowMaxAmount = true, tosState = TosState( tosLink = LegalState( title = stringReference("Terms of Use"), 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 c95f3ed693..ace4cb9006 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 @@ -99,8 +99,9 @@ internal class SwapViewModel @Inject constructor( private val lastAmount = mutableStateOf(INITIAL_AMOUNT) private var swapRouter: SwapRouter by Delegates.notNull() - private val isExchangeTooSmallAmountError: (SwapState) -> Boolean = { - it is SwapState.SwapError && it.error is DataError.ExchangeTooSmallAmountError + private val isUserResolvableError: (SwapState) -> Boolean = { + it is SwapState.SwapError && + (it.error is DataError.ExchangeTooSmallAmountError || it.error is DataError.ExchangeTooBigAmountError) } val currentScreen: SwapNavScreen @@ -1001,7 +1002,7 @@ internal class SwapViewModel @Inject constructor( private fun Map.consideredProvidersStates(): Map { return this.filter { - it.value is SwapState.QuotesLoadedState || isExchangeTooSmallAmountError(it.value) + it.value is SwapState.QuotesLoadedState || isUserResolvableError(it.value) } } diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 860b64eec4..81f6868424 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -51,6 +51,8 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.datasource) + implementation(projects.core.deepLinks) + implementation(projects.core.deepLinks.global) /** Domain modules */ implementation(projects.domain.appCurrency) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index 9c3c74dfd6..3a470f1a30 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -39,6 +39,7 @@ internal class TokenDetailsNotificationsAnalyticsSender( is TokenDetailsNotification.ExistentialDeposit, is TokenDetailsNotification.HasPendingTransactions, is TokenDetailsNotification.NetworksNoAccount, + is TokenDetailsNotification.TopUpWithoutReserve, is TokenDetailsNotification.RentInfo, is TokenDetailsNotification.SwapPromo, -> null diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index e880e6aa3e..8a4732ea16 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -147,6 +147,11 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ) + object TopUpWithoutReserve : Informational( + title = resourceReference(id = R.string.warning_no_account_title), + subtitle = resourceReference(id = R.string.no_account_bnb), + ) + class HasPendingTransactions(val coinSymbol: String) : Informational( title = resourceReference(R.string.warning_send_blocked_pending_transactions_title), subtitle = resourceReference( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index caad6173cc..c2a5ea5591 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification.* import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.removeBy @@ -19,13 +20,13 @@ internal class TokenDetailsNotificationConverter( fun removeRentInfo(currentState: TokenDetailsState): ImmutableList { val newNotifications = currentState.notifications.toMutableList() - newNotifications.removeBy { it is TokenDetailsNotification.RentInfo } + newNotifications.removeBy { it is RentInfo } return newNotifications.toImmutableList() } private fun mapToNotification(warning: CryptoCurrencyWarning): TokenDetailsNotification { return when (warning) { - is CryptoCurrencyWarning.BalanceNotEnoughForFee -> TokenDetailsNotification.NetworkFeeWithBuyButton( + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> NetworkFeeWithBuyButton( currency = warning.tokenCurrency, networkName = warning.coinCurrency.name, feeCurrencyName = warning.coinCurrency.name, @@ -35,7 +36,7 @@ internal class TokenDetailsNotificationConverter( is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { val feeCurrency = warning.feeCurrency if (feeCurrency != null) { - TokenDetailsNotification.NetworkFeeWithBuyButton( + NetworkFeeWithBuyButton( currency = warning.currency, networkName = feeCurrency.network.name, feeCurrencyName = warning.feeCurrencyName, @@ -43,7 +44,7 @@ internal class TokenDetailsNotificationConverter( onBuyClick = { clickIntents.onBuyCoinClick(feeCurrency) }, ) } else { - TokenDetailsNotification.NetworkFee( + NetworkFee( currency = warning.currency, networkName = warning.networkName, feeCurrencyName = warning.feeCurrencyName, @@ -51,23 +52,22 @@ internal class TokenDetailsNotificationConverter( ) } } - is CryptoCurrencyWarning.ExistentialDeposit -> TokenDetailsNotification.ExistentialDeposit( - existentialInfo = warning, - ) - is CryptoCurrencyWarning.Rent -> TokenDetailsNotification.RentInfo( + is CryptoCurrencyWarning.ExistentialDeposit -> ExistentialDeposit(existentialInfo = warning) + is CryptoCurrencyWarning.Rent -> RentInfo( rentInfo = warning, onCloseClick = clickIntents::onCloseRentInfoNotification, ) - CryptoCurrencyWarning.SomeNetworksUnreachable -> TokenDetailsNotification.NetworksUnreachable - is CryptoCurrencyWarning.SomeNetworksNoAccount -> TokenDetailsNotification.NetworksNoAccount( + CryptoCurrencyWarning.SomeNetworksUnreachable -> NetworksUnreachable + is CryptoCurrencyWarning.SomeNetworksNoAccount -> NetworksNoAccount( network = warning.amountCurrency.name, amount = warning.amountToCreateAccount.toString(), symbol = warning.amountCurrency.symbol, ) - is CryptoCurrencyWarning.HasPendingTransactions -> TokenDetailsNotification.HasPendingTransactions( + is CryptoCurrencyWarning.TopUpWithoutReserve -> TopUpWithoutReserve + is CryptoCurrencyWarning.HasPendingTransactions -> HasPendingTransactions( coinSymbol = warning.blockchainSymbol, ) - is CryptoCurrencyWarning.SwapPromo -> TokenDetailsNotification.SwapPromo( + is CryptoCurrencyWarning.SwapPromo -> SwapPromo( onSwapClick = clickIntents::onSwapPromoClick, onCloseClick = clickIntents::onSwapPromoDismiss, ) 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 7a7d53ba09..1fba010ecc 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 @@ -9,6 +9,9 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.deeplink.global.BuyCurrencyDeepLink +import com.tangem.core.deeplink.global.SellCurrencyDeepLink import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.resourceReference @@ -22,6 +25,7 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress @@ -87,6 +91,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, + deepLinksRegistry: DeepLinksRegistry, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -148,6 +153,34 @@ internal class TokenDetailsViewModel @Inject constructor( var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency)) private set + init { + deepLinksRegistry.registerWithViewModel( + viewModel = this, + deepLinks = listOf( + BuyCurrencyDeepLink(::onBuyCurrencyDeepLink), + SellCurrencyDeepLink(::onSellCurrencyDeepLink), + ), + ) + } + + private fun onBuyCurrencyDeepLink() { + val currency = cryptoCurrencyStatus?.currency ?: return + analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Bought(currency.symbol)) + } + + private fun onSellCurrencyDeepLink(data: SellCurrencyDeepLink.Data) { + sendCurrency( + status = cryptoCurrencyStatus ?: return, + transactionInfo = data.let { + TransactionInfo( + amount = it.baseCurrencyAmount, + transactionId = it.transactionId, + destinationAddress = it.depositWalletAddress, + ) + }, + ) + } + override fun onCreate(owner: LifecycleOwner) { analyticsEventsHandler.send( event = TokenScreenAnalyticsEvent.DetailsScreenOpened(token = cryptoCurrency.symbol), @@ -330,7 +363,7 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( - TradeCryptoAction.New.Buy( + TradeCryptoAction.Buy( userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, cryptoCurrencyStatus = status, appCurrencyCode = selectedAppCurrencyFlow.value.code, @@ -354,27 +387,31 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onSendClick() { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSend(cryptoCurrency.symbol)) - val cryptoCurrencyStatus = cryptoCurrencyStatus ?: return + sendCurrency(status = cryptoCurrencyStatus ?: return) + } + private fun sendCurrency(status: CryptoCurrencyStatus, transactionInfo: TransactionInfo? = null) { viewModelScope.launch(dispatchers.main) { val maybeFeeCurrencyStatus = - getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyStatus).getOrNull() + getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() - when (val currency = cryptoCurrencyStatus.currency) { + when (val currency = status.currency) { is CryptoCurrency.Coin -> { reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendCoin( + action = TradeCryptoAction.SendCoin( userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, - coinStatus = cryptoCurrencyStatus, + coinStatus = status, feeCurrencyStatus = maybeFeeCurrencyStatus, + transactionInfo = transactionInfo, ), ) } is CryptoCurrency.Token -> { sendToken( tokenCurrency = currency, - tokenFiatRate = cryptoCurrencyStatus.value.fiatRate, + tokenFiatRate = status.value.fiatRate, feeCurrencyStatus = maybeFeeCurrencyStatus, + transactionInfo = transactionInfo, ) } } @@ -385,6 +422,7 @@ internal class TokenDetailsViewModel @Inject constructor( tokenCurrency: CryptoCurrency.Token, tokenFiatRate: BigDecimal?, feeCurrencyStatus: CryptoCurrencyStatus?, + transactionInfo: TransactionInfo?, ) { viewModelScope.launch(dispatchers.io) { val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } @@ -399,7 +437,7 @@ internal class TokenDetailsViewModel @Inject constructor( .firstOrNull() reduxStateHolder.dispatchWithMain( - action = TradeCryptoAction.New.SendToken( + action = TradeCryptoAction.SendToken( userWallet = wallet, tokenCurrency = tokenCurrency, tokenFiatRate = tokenFiatRate, @@ -408,6 +446,7 @@ internal class TokenDetailsViewModel @Inject constructor( ifRight = { it.value.fiatRate }, ), feeCurrencyStatus = feeCurrencyStatus, + transactionInfo = transactionInfo, ), ) } @@ -440,7 +479,7 @@ internal class TokenDetailsViewModel @Inject constructor( val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse reduxStateHolder.dispatch( - TradeCryptoAction.New.Sell( + TradeCryptoAction.Sell( cryptoCurrencyStatus = status, appCurrencyCode = selectedAppCurrencyFlow.value.code, ), @@ -451,7 +490,7 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onSwapClick() { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrency.symbol)) - reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrency)) + reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrency)) } override fun onDismissDialog() { diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index a3207eb5cd..a24c30a994 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -49,6 +49,8 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.common) + implementation(projects.core.deepLinks) + implementation(projects.core.deepLinks.global) /** Domain modules */ implementation(projects.domain.card) @@ -67,6 +69,7 @@ dependencies { implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.analytics) + implementation(projects.domain.visa) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 9a3d17502f..9c6177c05e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -7,7 +7,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed -import androidx.compose.ui.layout.* +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -123,8 +126,9 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier Layout(content = content, modifier = modifier) { measurables, constraints -> val layoutWidth = constraints.maxWidth - val layoutPadding = with(density) { dimens.size14.roundToPx() } - val layoutWidthWithoutPaddings = layoutWidth - 2 * layoutPadding + val horizontalPadding = with(density) { dimens.size14.roundToPx() } + val verticalPadding = with(density) { dimens.size16.roundToPx() } + val layoutWidthWithoutPaddings = layoutWidth - 2 * horizontalPadding val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() val priceChangeMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt() @@ -209,7 +213,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier val layoutHeight = calculateLayoutHeight( state = state, minLayoutHeight = with(density) { dimens.size68.roundToPx() }, - layoutPadding = layoutPadding, + layoutPadding = horizontalPadding, betweenRowsPadding = with(density) { dimens.size2.roundToPx() }, title = title, fiatAmount = fiatAmount, @@ -218,35 +222,35 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier ) layout(width = constraints.maxWidth, height = layoutHeight) { - icon.placeRelative(x = layoutPadding, y = (layoutHeight - icon.height).div(other = 2)) + icon.placeRelative(x = horizontalPadding, y = (layoutHeight - icon.height).div(other = 2)) title.placeRelative( - x = layoutPadding + icon.width, + x = horizontalPadding + icon.width, y = when (state) { is TokenItemState.NoAddress, is TokenItemState.Unreachable, -> (layoutHeight - title.height).div(other = 2) - else -> layoutPadding + else -> verticalPadding }, ) - fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - layoutPadding, y = layoutPadding) + fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - horizontalPadding, y = verticalPadding) priceChange?.placeRelative( - x = layoutPadding + icon.width, - y = layoutHeight - priceChange.height - layoutPadding, + x = horizontalPadding + icon.width, + y = layoutHeight - priceChange.height - verticalPadding, ) cryptoAmount?.placeRelative( x = when (state) { - is TokenItemState.Draggable -> layoutPadding + icon.width - else -> layoutWidth - cryptoAmount.width - layoutPadding + is TokenItemState.Draggable -> horizontalPadding + icon.width + else -> layoutWidth - cryptoAmount.width - horizontalPadding }, - y = layoutHeight - cryptoAmount.height - layoutPadding, + y = layoutHeight - cryptoAmount.height - verticalPadding, ) nonFiatContent.placeRelative( - x = layoutWidth - nonFiatContent.width - layoutPadding, + x = layoutWidth - nonFiatContent.width - horizontalPadding, y = (layoutHeight - nonFiatContent.height).div(other = 2), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt new file mode 100644 index 0000000000..0248645642 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt @@ -0,0 +1,156 @@ +package com.tangem.feature.wallet.presentation.deeplink + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.deeplink.DeepLink +import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.deeplink.global.BuyCurrencyDeepLink +import com.tangem.core.deeplink.global.SellCurrencyDeepLink +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyUseCase +import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class WalletDeepLinksHandler @Inject constructor( + private val deepLinksRegistry: DeepLinksRegistry, + private val analyticsEventHandler: AnalyticsEventHandler, + private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, + private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, + private val reduxStateHolder: ReduxStateHolder, +) { + + private var deepLinks: List = emptyList() + + fun registerForSingleCurrencyWallets(viewModel: ViewModel, userWallet: UserWallet) { + if (userWallet.isMultiCurrency) { + deepLinksRegistry.unregister(deepLinks) + } else { + if (deepLinks.isEmpty()) { + deepLinks = getDeepLinks(userWallet, viewModel.viewModelScope) + } + + deepLinksRegistry.register(deepLinks) + } + + viewModel.addCloseable { + deepLinksRegistry.unregister(deepLinks) + } + } + + private fun getDeepLinks(userWallet: UserWallet, scope: CoroutineScope): List { + val sellCurrencyDeepLink = SellCurrencyDeepLink( + onReceive = { data -> + scope.launch { + onSellCurrencyDeepLink(userWallet, data) + } + }, + ) + val buyCurrencyDeepLink = BuyCurrencyDeepLink( + onReceive = { + scope.launch { + onBuyCurrencyDeepLink(userWallet) + } + }, + ) + + return listOf(sellCurrencyDeepLink, buyCurrencyDeepLink) + } + + private suspend fun onSellCurrencyDeepLink(userWallet: UserWallet, data: SellCurrencyDeepLink.Data) { + val cryptoCurrencyStatus = getCryptoCurrencyStatusSyncUseCase(userWallet.walletId) + .getOrNull() ?: return + val feeCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWallet.walletId, + cryptoCurrencyStatus, + ).getOrNull() + + val transactionInfo = data.let { + TransactionInfo( + amount = it.baseCurrencyAmount, + destinationAddress = it.depositWalletAddress, + transactionId = it.transactionId, + ) + } + + when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> sendCoin( + userWallet = userWallet, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + transactionInfo = transactionInfo, + ) + is CryptoCurrency.Token -> sendToken( + userWallet = userWallet, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + transactionInfo = transactionInfo, + ) + } + } + + private suspend fun onBuyCurrencyDeepLink(userWallet: UserWallet) { + val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId).getOrNull() ?: return + + analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol)) + } + + private fun sendCoin( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + transactionInfo: TransactionInfo, + ) { + reduxStateHolder.dispatch( + action = TradeCryptoAction.SendCoin( + userWallet = userWallet, + coinStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + transactionInfo = transactionInfo, + ), + ) + } + + private suspend fun sendToken( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + transactionInfo: TransactionInfo, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return + val coinStatus = getNetworkCoinStatusUseCase( + userWalletId = userWallet.walletId, + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.network.derivationPath, + isSingleWalletWithTokens = false, + ) + .firstOrNull() + ?.getOrNull() + ?: return + + reduxStateHolder.dispatch( + action = TradeCryptoAction.SendToken( + userWallet = userWallet, + tokenCurrency = cryptoCurrency, + tokenFiatRate = cryptoCurrencyStatus.value.fiatRate, + coinFiatRate = coinStatus.value.fiatRate, + feeCurrencyStatus = feeCurrencyStatus, + transactionInfo = transactionInfo, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index 963970f85a..7c7adbde20 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -31,6 +31,9 @@ internal object WalletImageResolver { cardTypesResolver.isTronWallet() -> userWallet.resolveTronWallet() cardTypesResolver.isKaspaWallet() -> userWallet.resolveKaspaWallet() cardTypesResolver.isBadWallet() -> userWallet.resolveBadWallet() + cardTypesResolver.isJrWallet() -> userWallet.resolveJrWallet() + cardTypesResolver.isGrimWallet() -> userWallet.resolveGrimWallet() + cardTypesResolver.isSatoshiFriendsWallet() -> userWallet.resolveSatoshiWallet() cardTypesResolver.isWallet2() -> userWallet.resolveWallet2() cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet() cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1() @@ -78,6 +81,27 @@ internal object WalletImageResolver { ) } + private fun UserWallet.resolveJrWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_jr_card2_120_106, + twoBackupResId = R.drawable.ill_jr_card3_120_106, + ) + } + + private fun UserWallet.resolveGrimWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_grim_card2_120_106, + twoBackupResId = R.drawable.ill_grim_card3_120_106, + ) + } + + private fun UserWallet.resolveSatoshiWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_satoshi_card2_120_106, + twoBackupResId = R.drawable.ill_satoshi_card3_120_106, + ) + } + private fun UserWallet.resolveShibaWallet(): Int? { return resolveWallet2( oneBackupResId = R.drawable.ill_shiba_card2_120_106, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt index 8a221a2d24..7d41dcb257 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt @@ -1,14 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.visa.GetVisaCurrencyUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.PrimaryCurrencySubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.TxHistorySubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.VisaWalletBalancesAndLimitsSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber @@ -21,24 +18,20 @@ internal class VisaWalletContentLoader( private val isRefresh: Boolean, private val stateHolder: WalletStateController, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, + private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { return listOf( - PrimaryCurrencySubscriber( + VisaWalletBalancesAndLimitsSubscriber( userWallet = userWallet, stateHolder = stateHolder, - getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, - setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, - analyticsEventHandler = analyticsEventHandler, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, + isRefresh = isRefresh, + getVisaCurrencyUseCase = getVisaCurrencyUseCase, + clickIntents = clickIntents, ), - VisaWalletBalancesAndLimitsSubscriber(userWallet, stateHolder, clickIntents), TxHistorySubscriber( userWallet = userWallet, isRefresh = isRefresh, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt index 61db482fa2..e81f30afff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt @@ -1,11 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.visa.GetVisaCurrencyUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 @@ -17,11 +15,9 @@ import javax.inject.Inject internal class VisaWalletContentLoaderFactory @Inject constructor( private val stateHolder: WalletStateController, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, + private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntentsV2, isRefresh: Boolean): WalletContentLoader { @@ -31,11 +27,9 @@ internal class VisaWalletContentLoaderFactory @Inject constructor( isRefresh = isRefresh, stateHolder = stateHolder, getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, - setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, txHistoryItemsUseCase = txHistoryItemsUseCase, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - analyticsEventHandler = analyticsEventHandler, + getVisaCurrencyUseCase = getVisaCurrencyUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/model/BalancesAndLimitsBlockState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/model/BalancesAndLimitsBlockState.kt index 883ce0b468..a64e659900 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/model/BalancesAndLimitsBlockState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/model/BalancesAndLimitsBlockState.kt @@ -11,7 +11,6 @@ internal sealed class BalancesAndLimitsBlockState { data class Content( val availableBalance: String, - val currencySymbol: String, val limitDays: Int, val isEnabled: Boolean, val onClick: () -> Unit, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetBalancesAndLimitsTransformer.kt index 4092298ae5..9af5df2c08 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetBalancesAndLimitsTransformer.kt @@ -1,32 +1,98 @@ package com.tangem.feature.wallet.presentation.wallet.state2.transformers +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletAdditionalInfo +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state2.model.BalancesAndLimitsBlockState import com.tangem.feature.wallet.presentation.wallet.state2.model.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import org.joda.time.DateTime +import org.joda.time.Days internal class SetBalancesAndLimitsTransformer( - userWallet: UserWallet, + private val userWallet: UserWallet, + private val maybeVisaCurrency: Either, private val clickIntents: WalletClickIntentsV2, ) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { return prevState.transformWhenInState { state -> + val visaCurrency = maybeVisaCurrency.getOrElse { + return state.copy( + walletCardState = getErrorWalletCardState(state.walletCardState), + balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, + ) + } + state.copy( - balancesAndLimitBlockState = state.balancesAndLimitBlockState.toLoadedState(), + walletCardState = getContentWalletCardState(state.walletCardState, visaCurrency), + balancesAndLimitBlockState = getContentBlockState(visaCurrency), ) } } - // TODO: Implement in [REDACTED_JIRA] - @Suppress("UnusedReceiverParameter") - private fun BalancesAndLimitsBlockState.toLoadedState(): BalancesAndLimitsBlockState { - return BalancesAndLimitsBlockState.Content( - availableBalance = "400.00", - currencySymbol = "USDT", - limitDays = 7, - isEnabled = true, - onClick = clickIntents::onBalancesAndLimitsClick, + private fun getContentBlockState(visaCurrency: VisaCurrency) = BalancesAndLimitsBlockState.Content( + availableBalance = BigDecimalFormatter.formatCryptoAmount( + visaCurrency.limits.remainingOtp, + visaCurrency.symbol, + visaCurrency.decimals, + ), + limitDays = Days.daysBetween(DateTime.now(), visaCurrency.limits.expirationDate).days.inc(), + isEnabled = true, + onClick = clickIntents::onBalancesAndLimitsClick, + ) + + private fun getErrorWalletCardState(prevState: WalletCardState): WalletCardState { + return with(prevState) { + WalletCardState.Error( + id = id, + title = title, + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + } + + private fun getContentWalletCardState(prevState: WalletCardState, visaCurrency: VisaCurrency): WalletCardState { + return with(prevState) { + WalletCardState.Content( + id = id, + title = title, + additionalInfo = createAdditionalInfo(visaCurrency), + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + balance = BigDecimalFormatter.formatCryptoAmount( + visaCurrency.balances.available, + visaCurrency.symbol, + visaCurrency.decimals, + ), + cardCount = userWallet.getCardsCount(), + ) + } + } + + private fun createAdditionalInfo(visaCurrency: VisaCurrency): WalletAdditionalInfo { + val fiatAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = visaCurrency.fiatRate?.let { visaCurrency.balances.available.multiply(it) }, + fiatCurrencyCode = visaCurrency.fiatCurrency.code, + fiatCurrencySymbol = visaCurrency.fiatCurrency.symbol, ) + val infoContent = stringReference( + value = buildString { + append(fiatAmount) + append(" • ") + append(visaCurrency.networkName) + }, + ) + + return WalletAdditionalInfo(hideable = true, infoContent) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt index ade63f113c..da21391c07 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt @@ -8,7 +8,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCard import com.tangem.feature.wallet.presentation.wallet.state2.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletMarketPriceConverter -import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.VisaWalletCardStateConverter import timber.log.Timber internal class SetPrimaryCurrencyTransformer( @@ -25,15 +24,11 @@ internal class SetPrimaryCurrencyTransformer( marketPriceBlockState = prevState.marketPriceBlockState.toLoadedState(), ) } - is WalletState.Visa.Content -> { - prevState.copy( - walletCardState = prevState.walletCardState.toLoadedVisaState(), - depositButtonState = prevState.depositButtonState.copy(isEnabled = true), - ) + is WalletState.Visa -> { + Timber.w("Impossible to load primary currency status for VISA wallet") + prevState } - is WalletState.Visa.Locked, - is WalletState.SingleCurrency.Locked, - -> { + is WalletState.SingleCurrency.Locked -> { Timber.w("Impossible to load primary currency status for locked wallet") prevState } @@ -48,10 +43,6 @@ internal class SetPrimaryCurrencyTransformer( return SingleWalletCardStateConverter(status.value, userWallet, appCurrency).convert(value = this) } - private fun WalletCardState.toLoadedVisaState(): WalletCardState { - return VisaWalletCardStateConverter(status, userWallet, appCurrency).convert(value = this) - } - private fun MarketPriceBlockState.toLoadedState(): MarketPriceBlockState { return SingleWalletMarketPriceConverter(status.value, appCurrency).convert(value = this) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt new file mode 100644 index 0000000000..2a6139aa60 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt @@ -0,0 +1,52 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.model.BalancesAndLimitsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class BalancesAndLimitsBottomSheetConverter( + private val eventSender: WalletEventSender, +) : Converter { + + override fun convert(value: VisaCurrency): BalancesAndLimitsBottomSheetConfig { + return BalancesAndLimitsBottomSheetConfig( + currency = value.symbol, + balance = BalancesAndLimitsBottomSheetConfig.Balance( + totalBalance = value.balances.total.let(::formatAmount), + availableBalance = value.balances.available.let(::formatAmount), + blockedBalance = value.balances.blocked.let(::formatAmount), + debit = value.balances.debt.let(::formatAmount), + pending = value.balances.pendingRefund.let(::formatAmount), + amlVerified = value.balances.verified.let(::formatAmount), + ), + limit = BalancesAndLimitsBottomSheetConfig.Limit( + availableBy = DateTimeFormatters.formatDate(date = value.limits.expirationDate), + inStore = value.limits.remainingOtp.let(::formatAmount), + other = value.limits.remainingNoOtp.let(::formatAmount), + singleTransaction = value.limits.singleTransaction.let(::formatAmount), + ), + onBalanceInfoClick = this::showBalanceInfo, + onLimitInfoClick = this::showLimitInfo, + ) + } + + private fun formatAmount(amount: BigDecimal): String = BigDecimalFormatter.formatCryptoAmount( + amount, + cryptoCurrency = "", + decimals = 2, + ) + + private fun showBalanceInfo() { + eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo)) + } + + private fun showLimitInfo() { + eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo)) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/VisaWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/VisaWalletCardStateConverter.kt deleted file mode 100644 index 56766efc40..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/VisaWalletCardStateConverter.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter - -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 -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletAdditionalInfo -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState -import com.tangem.utils.converter.Converter - -internal class VisaWalletCardStateConverter( - private val status: CryptoCurrencyStatus, - private val selectedWallet: UserWallet, - private val appCurrency: AppCurrency, -) : Converter { - - override fun convert(value: WalletCardState): WalletCardState { - return when (status.value) { - is CryptoCurrencyStatus.Loading -> value.toLoadingState() - is CryptoCurrencyStatus.Custom, - is CryptoCurrencyStatus.MissedDerivation, - is CryptoCurrencyStatus.Unreachable, - -> value.toErrorState() - is CryptoCurrencyStatus.NoQuote, - is CryptoCurrencyStatus.Loaded, - is CryptoCurrencyStatus.NoAccount, - is CryptoCurrencyStatus.NoAmount, - -> value.toContentState(status) - } - } - - private fun WalletCardState.toLoadingState(): WalletCardState { - return WalletCardState.Loading( - id = id, - title = title, - imageResId = imageResId, - onRenameClick = onRenameClick, - onDeleteClick = onDeleteClick, - ) - } - - private fun WalletCardState.toErrorState(): WalletCardState { - return WalletCardState.Error( - id = id, - title = title, - imageResId = imageResId, - onRenameClick = onRenameClick, - onDeleteClick = onDeleteClick, - ) - } - - private fun WalletCardState.toContentState(status: CryptoCurrencyStatus): WalletCardState { - return WalletCardState.Content( - id = id, - title = title, - additionalInfo = createAdditionalInfo(status), - imageResId = imageResId, - onRenameClick = onRenameClick, - onDeleteClick = onDeleteClick, - balance = formatAmount(status), - cardCount = selectedWallet.getCardsCount(), - ) - } - - private fun createAdditionalInfo(status: CryptoCurrencyStatus): WalletAdditionalInfo { - val fiatAmount = BigDecimalFormatter.formatFiatAmount( - status.value.fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - val infoContent = stringReference( - value = buildString { - append(fiatAmount) - append(" • ") - append(status.currency.network.name) - }, - ) - - return WalletAdditionalInfo(hideable = true, infoContent) - } - - private fun formatAmount(status: CryptoCurrencyStatus): String { - val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN - - return BigDecimalFormatter.formatCryptoAmount(amount, status.currency) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt index a1e47bf291..d9d435e6f0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -3,6 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import androidx.paging.PagingData import androidx.paging.cachedIn import arrow.core.Either +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryItem @@ -13,10 +15,8 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryCountErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryCountTransformer -import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryItemsErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryItemsTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.* import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -37,6 +37,24 @@ internal class TxHistorySubscriber( ) : WalletSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow> { + // TODO: [REDACTED_JIRA] + if (userWallet.scanResponse.cardTypesResolver.isVisaWallet()) { + return flow { + stateHolder.update( + object : WalletStateTransformer(userWallet.walletId) { + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.Visa.Content -> prevState.copy( + txHistoryState = TxHistoryState.Empty(onExploreClick = {}), + ) + else -> prevState + } + } + }, + ) + } + } + return flow { getPrimaryCurrencyStatusUpdatesUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> val maybeTxHistoryItemCount = txHistoryItemsCountUseCase( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletBalancesAndLimitsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletBalancesAndLimitsSubscriber.kt index af94827eb2..a25af15cdc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletBalancesAndLimitsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletBalancesAndLimitsSubscriber.kt @@ -1,23 +1,32 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.domain.visa.GetVisaCurrencyUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetBalancesAndLimitsTransformer import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flow +@Suppress("LongParameterList") internal class VisaWalletBalancesAndLimitsSubscriber( private val userWallet: UserWallet, private val stateHolder: WalletStateController, + private val isRefresh: Boolean, + private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, private val clickIntents: WalletClickIntentsV2, ) : WalletSubscriber() { - // TODO: Implement in [REDACTED_JIRA] - override fun create(coroutineScope: CoroutineScope): Flow<*> = suspend { - delay(timeMillis = 500) - stateHolder.update(SetBalancesAndLimitsTransformer(userWallet, clickIntents)) - }.asFlow() + override fun create(coroutineScope: CoroutineScope): Flow<*> { + return flow { + stateHolder.update( + SetBalancesAndLimitsTransformer( + userWallet = userWallet, + maybeVisaCurrency = getVisaCurrencyUseCase(userWallet.walletId, isRefresh), + clickIntents = clickIntents, + ), + ) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt index 26ede92649..5855ab22f6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt @@ -113,7 +113,6 @@ private fun Content(state: BalancesAndLimitsBlockState, modifier: Modifier = Mod is BalancesAndLimitsBlockState.Content -> with(blockState) { AvailableLimit( availableBalance = availableBalance, - currencySymbol = currencySymbol, limitDays = limitDays, ) } @@ -136,19 +135,14 @@ private fun Content(state: BalancesAndLimitsBlockState, modifier: Modifier = Mod } @Composable -private fun AvailableLimit( - availableBalance: String, - currencySymbol: String, - limitDays: Int, - modifier: Modifier = Modifier, -) { +private fun AvailableLimit(availableBalance: String, limitDays: Int, modifier: Modifier = Modifier) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { Text( - text = "$availableBalance $currencySymbol", + text = availableBalance, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, ) @@ -186,8 +180,7 @@ private class BalancesAndLimitsBlockParameterProvider : CollectionPreviewParamet BalancesAndLimitsBlockState.Loading, BalancesAndLimitsBlockState.Error, BalancesAndLimitsBlockState.Content( - availableBalance = "400.00", - currencySymbol = "USDT", + availableBalance = "400.00 USDT", limitDays = 7, isEnabled = true, onClick = {}, 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 065a47b196..8adfe05590 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 @@ -609,7 +609,7 @@ internal class WalletViewModel @Inject constructor( showErrorIfDemoModeOrElse { reduxStateHolder.dispatch( - TradeCryptoAction.New.Buy( + TradeCryptoAction.Buy( userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex), cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrencyCode = selectedAppCurrencyFlow.value.code, @@ -623,7 +623,7 @@ internal class WalletViewModel @Inject constructor( event = TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrencyStatus.currency.symbol), ) - reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrencyStatus.currency)) + reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrencyStatus.currency)) } override fun onSingleCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus?) { @@ -641,7 +641,7 @@ internal class WalletViewModel @Inject constructor( val maybeFeeCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWallet.walletId, coinStatus).getOrNull() reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendCoin( + action = TradeCryptoAction.SendCoin( userWallet = userWallet, coinStatus = coinStatus, feeCurrencyStatus = maybeFeeCurrencyStatus, @@ -665,7 +665,7 @@ internal class WalletViewModel @Inject constructor( is CryptoCurrency.Coin -> { uiState = stateFactory.getStateWithClosedBottomSheet() reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendCoin( + action = TradeCryptoAction.SendCoin( userWallet = userWallet, coinStatus = cryptoCurrencyStatus, feeCurrencyStatus = maybeFeeCurrencyStatus, @@ -694,7 +694,7 @@ internal class WalletViewModel @Inject constructor( it.onRight { coinStatus -> uiState = stateFactory.getStateWithClosedBottomSheet() reduxStateHolder.dispatchWithMain( - action = TradeCryptoAction.New.SendToken( + action = TradeCryptoAction.SendToken( userWallet = userWallet, tokenCurrency = requireNotNull(cryptoCurrencyStatus.currency as? CryptoCurrency.Token), tokenFiatRate = cryptoCurrencyStatus.value.fiatRate, @@ -772,7 +772,7 @@ internal class WalletViewModel @Inject constructor( showErrorIfDemoModeOrElse { reduxStateHolder.dispatch( - action = TradeCryptoAction.New.Sell( + action = TradeCryptoAction.Sell( cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrencyCode = selectedAppCurrencyFlow.value.code, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt index ae097c7991..418da4487e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt @@ -13,6 +13,7 @@ import com.tangem.domain.walletconnect.WalletConnectActions import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender @@ -57,6 +58,7 @@ internal class WalletViewModelV2 @Inject constructor( private val reduxStateHolder: ReduxStateHolder, private val screenLifecycleProvider: ScreenLifecycleProvider, private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, + private val walletDeepLinksHandler: WalletDeepLinksHandler, ) : ViewModel() { val uiState: StateFlow = stateHolder.uiState @@ -157,6 +159,11 @@ internal class WalletViewModelV2 @Inject constructor( selectedWalletAnalyticsSender.send(selectedWallet) } + + walletDeepLinksHandler.registerForSingleCurrencyWallets( + viewModel = this, + userWallet = selectedWallet, + ) } .flowOn(dispatchers.main) .launchIn(viewModelScope) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/VisaWalletIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/VisaWalletIntents.kt index fc0b879db4..5dad048ccd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/VisaWalletIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/VisaWalletIntents.kt @@ -4,18 +4,16 @@ import arrow.core.getOrElse import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.visa.GetVisaCurrencyUseCase import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState -import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state2.model.BalancesAndLimitsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.BalancesAndLimitsBottomSheetConverter import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -31,10 +29,15 @@ internal interface VisaWalletIntents { internal class VisaWalletIntentsImplementor @Inject constructor( private val stateController: WalletStateController, private val eventSender: WalletEventSender, - private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), VisaWalletIntents { + private val balancesAndLimitsBottomSheetConverter by lazy(mode = LazyThreadSafetyMode.NONE) { + BalancesAndLimitsBottomSheetConverter(eventSender) + } + override fun onDepositClick() { val userWalletId = stateController.getSelectedWalletId() @@ -47,15 +50,6 @@ internal class VisaWalletIntentsImplementor @Inject constructor( } } - private suspend fun getPrimaryCurrencyStatus(userWalletId: UserWalletId): CryptoCurrencyStatus? { - return getPrimaryCurrencyUseCase(userWalletId) - .firstOrNull() - ?.getOrElse { - Timber.e("Failed to get primary currency $it") - null - } - } - private fun createReceiveBottomSheetContent(currencyStatus: CryptoCurrencyStatus): TangemBottomSheetConfigContent? { val currency = currencyStatus.currency val addresses = currencyStatus.value.networkAddress?.availableAddresses @@ -76,35 +70,27 @@ internal class VisaWalletIntentsImplementor @Inject constructor( } override fun onBalancesAndLimitsClick() { - stateController.showBottomSheet(getBalancesAndLimitsConfig()) + viewModelScope.launch(dispatchers.main) { + val userWalletId = stateController.getSelectedWalletId() + val balancesAndLimits = getVisaCurrencyUseCase(userWalletId) + .getOrElse { + Timber.e("Unable to get balances and limits: $it") + return@launch + } + + val bottomSheetContent = balancesAndLimitsBottomSheetConverter.convert( + value = balancesAndLimits, + ) + + stateController.showBottomSheet(bottomSheetContent) + } } - // TODO: Implement - private fun getBalancesAndLimitsConfig() = BalancesAndLimitsBottomSheetConfig( - currency = "USDT", - balance = BalancesAndLimitsBottomSheetConfig.Balance( - totalBalance = "492.45", - availableBalance = "392.45", - blockedBalance = "36.00", - debit = "00.00", - pending = "20.99", - amlVerified = "356.45", - ), - limit = BalancesAndLimitsBottomSheetConfig.Limit( - availableBy = "Nov, 11", - inStore = "563.00", - other = "100.00", - singleTransaction = "100.00", - ), - onBalanceInfoClick = this::showBalanceInfo, - onLimitInfoClick = this::showLimitInfo, - ) - - private fun showBalanceInfo() { - eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo)) - } - - private fun showLimitInfo() { - eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo)) + private suspend fun getPrimaryCurrencyStatus(userWalletId: UserWalletId): CryptoCurrencyStatus? { + return getCurrencyStatusUseCase(userWalletId) + .getOrElse { + Timber.e("Failed to get primary currency $it") + null + } } } \ No newline at end of file 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 b4e85a8422..25c2b29527 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 @@ -111,7 +111,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( feeCurrencyStatus: CryptoCurrencyStatus?, ) { reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendCoin( + action = TradeCryptoAction.SendCoin( userWallet = userWallet, coinStatus = cryptoCurrencyStatus, feeCurrencyStatus = feeCurrencyStatus, @@ -136,7 +136,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( .collectLatest { it.onRight { coinStatus -> reduxStateHolder.dispatch( - action = TradeCryptoAction.New.SendToken( + action = TradeCryptoAction.SendToken( userWallet = userWallet, tokenCurrency = cryptoCurrency, tokenFiatRate = cryptoCurrencyStatus.fiatRate, @@ -282,7 +282,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse { viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( - action = TradeCryptoAction.New.Sell( + action = TradeCryptoAction.Sell( cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, ), @@ -301,7 +301,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse { viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( - TradeCryptoAction.New.Buy( + TradeCryptoAction.Buy( userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, @@ -316,7 +316,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( event = TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrencyStatus.currency.symbol), ) - reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrencyStatus.currency)) + reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrencyStatus.currency)) } override fun onExploreClick() { diff --git a/features/wallet/impl/src/main/res/drawable/ill_grim_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_grim_card2_120_106.webp new file mode 100644 index 0000000000..5568ac6e06 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_grim_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_grim_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_grim_card3_120_106.webp new file mode 100644 index 0000000000..f58a7467c0 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_grim_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_jr_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_jr_card2_120_106.webp new file mode 100644 index 0000000000..d4efc8c070 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_jr_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_jr_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_jr_card3_120_106.webp new file mode 100644 index 0000000000..76efd29547 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_jr_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_satoshi_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_satoshi_card2_120_106.webp new file mode 100644 index 0000000000..31310554b3 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_satoshi_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_satoshi_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_satoshi_card3_120_106.webp new file mode 100644 index 0000000000..3652d41e72 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_satoshi_card3_120_106.webp differ diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 11552dabef..826d5cf01f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,9 +85,9 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.5-469" +tangemBlockchainSdk = "release-app_5.6-472" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.5-323" +tangemCardSdk = "release-app_5.6-325" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem diff --git a/settings.gradle.kts b/settings.gradle.kts index edf4844e3e..7fc4e85cba 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,6 +70,8 @@ include(":core:navigation") include(":core:res") include(":core:ui") include(":core:utils") +include(":core:deep-links") +include(":core:deep-links:global") // endregion Core modules // region Libs modules @@ -133,6 +135,7 @@ include(":domain:balance-hiding") include(":domain:balance-hiding:models") include(":domain:transaction") include(":domain:analytics") +include(":domain:visa") // endregion Domain modules // region Data modules @@ -148,4 +151,5 @@ include(":data:txhistory") include(":data:wallets") include(":data:analytics") include(":data:transaction") -// endregion Data modules +include(":data:visa") +// endregion Data modules \ No newline at end of file