diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 3687dc92e7..1eab5c0eb6 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -5,6 +5,7 @@ import com.squareup.moshi.Moshi import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectMapSync import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.visa.TangemPayStorage @@ -19,6 +20,7 @@ import javax.inject.Singleton private const val DEFAULT_KEY = "tangem_pay_default_key" private const val ORDER_ID_KEY = "tangem_pay_order_id_key" +private const val WITHDRAW_ORDER_ID_KEY = "tangem_pay_withdraw_order_id_key" @Singleton internal class DefaultTangemPayStorage @Inject constructor( @@ -114,9 +116,38 @@ internal class DefaultTangemPayStorage @Inject constructor( appPreferencesStore.store(PreferencesKeys.getTangemPayAddToWalletKey(customerWalletAddress), false) } + override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) { + appPreferencesStore.editData { mutablePreferences -> + val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) + .plus(createWithdrawOrderIdKey(userWalletId) to orderId) + mutablePreferences.setObjectMap( + key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + value = orders, + ) + } + } + + override suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String? { + val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) + return orders[createWithdrawOrderIdKey(userWalletId)] + } + + override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) { + appPreferencesStore.editData { mutablePreferences -> + val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) + .minus(createWithdrawOrderIdKey(userWalletId)) + mutablePreferences.setObjectMap( + key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + value = orders, + ) + } + } + private fun createCustomerAddressKey(userWalletId: UserWalletId): String = userWalletId.stringValue private fun createKey(address: String): String = "${DEFAULT_KEY}_$address" private fun createOrderIdKey(address: String): String = "${ORDER_ID_KEY}_$address" + + private fun createWithdrawOrderIdKey(userWalletId: UserWalletId): String = "${WITHDRAW_ORDER_ID_KEY}_$userWalletId" } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 89c571cd1e..a303cd8df9 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -11,6 +11,7 @@ import com.tangem.common.authentication.AuthenticationManager import com.tangem.common.authentication.keystore.KeystoreManager import com.tangem.common.core.* import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.hexToBytes import com.tangem.common.services.secure.SecureStorage import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics @@ -45,6 +46,7 @@ import com.tangem.tap.domain.tasks.product.ResetBackupCardTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask +import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask @@ -524,6 +526,15 @@ internal class DefaultTangemSdkManager( } } + override suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult { + return coroutineScope { + runTaskAsyncReturnOnMain( + runnable = TangemPaySignWithdrawalHashTask(cardId = cardId, hash = hash.hexToBytes()), + cardId = cardId, + initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), + ) + } + } // endregion companion object { diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index d86ebd543b..91fe1414d6 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -219,5 +219,9 @@ class MockTangemSdkManager( error("Not implemented") } + override suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult { + error("Not implemented") + } + // endregion } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt new file mode 100644 index 0000000000..faf32983a0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/TangemPaySignWithdrawalHashTask.kt @@ -0,0 +1,121 @@ +package com.tangem.tap.domain.tasks.visa + +import com.tangem.blockchain.common.UnmarshalHelper +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.common.core.CompletionCallback +import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.toDecompressedPublicKey +import com.tangem.common.extensions.toHexString +import com.tangem.core.error.ext.tangemError +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.card.common.visa.VisaUtilities +import com.tangem.domain.visa.error.VisaActivationError +import com.tangem.operations.ScanTask +import com.tangem.operations.derivation.DeriveWalletPublicKeyTask +import com.tangem.operations.sign.SignHashCommand + +class TangemPaySignWithdrawalHashTask( + private val cardId: String, + private val hash: ByteArray, +) : CardSessionRunnable { + + override fun run(session: CardSession, callback: CompletionCallback) { + val card = session.environment.card ?: run { + callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) + return + } + + if (card.cardId != cardId) { + callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError)) + return + } + + proceedSign(card, session, callback) + } + + private fun proceedSign(card: Card, session: CardSession, callback: CompletionCallback) { + val derivationPath = VisaUtilities.customDerivationPath + + val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { + callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)) + return + } + + val derivationTask = DeriveWalletPublicKeyTask( + walletPublicKey = wallet.publicKey, + derivationPath = derivationPath, + ) + + derivationTask.run(session) { result -> + when (result) { + is CompletionResult.Success -> { + signData( + targetWalletPublicKey = wallet.publicKey, + derivationPath = derivationPath, + session = session, + extendedPublicKey = result.data, + callback = callback, + ) + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + } + + private fun signData( + targetWalletPublicKey: ByteArray, + derivationPath: DerivationPath?, + extendedPublicKey: ExtendedPublicKey?, + session: CardSession, + callback: CompletionCallback, + ) { + val signTask = SignHashCommand( + hash = hash, + walletPublicKey = targetWalletPublicKey, + derivationPath = derivationPath, + ) + + signTask.run(session) { result -> + when (result) { + is CompletionResult.Success -> { + val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( + signature = result.data.signature, + hash = hash, + publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() + ?: targetWalletPublicKey.toDecompressedPublicKey(), + ).asRSVLegacyEVM().toHexString().lowercase() + + scanCard( + session = session, + callback = callback, + signedData = rsvSignature, + ) + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + } + + private fun scanCard(signedData: String, session: CardSession, callback: CompletionCallback) { + val scanTask = ScanTask() + scanTask.run(session) { result -> + when (result) { + is CompletionResult.Success -> { + callback(CompletionResult.Success(signedData)) + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 5a48848579..57dc3d376e 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -328,6 +328,7 @@ internal class ChildFactory @Inject constructor( cryptoAmount = tangemPayInput.cryptoAmount, fiatAmount = tangemPayInput.fiatAmount, depositAddress = tangemPayInput.depositAddress, + isWithdrawal = tangemPayInput.isWithdrawal, ) }, ), diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 1428522f9f..f1c07144f6 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -197,6 +197,7 @@ sealed class AppRoute(val path: String) : Route { val cryptoAmount: SerializedBigDecimal, val fiatAmount: SerializedBigDecimal, val depositAddress: String, + val isWithdrawal: Boolean, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 116560b40e..9c4fca8149 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -166,4 +166,16 @@ interface TangemPayApi { @Header("Authorization") authHeader: String, @Body body: FreezeUnfreezeCardRequest, ): ApiResponse + + @POST("v1/customer/card/withdraw/data") + suspend fun getWithdrawData( + @Header("Authorization") authHeader: String, + @Body body: WithdrawDataRequest, + ): ApiResponse + + @POST("v1/customer/card/withdraw") + suspend fun withdraw( + @Header("Authorization") authHeader: String, + @Body body: WithdrawRequest, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/WithdrawDataRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/WithdrawDataRequest.kt new file mode 100644 index 0000000000..20d3ecfd02 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/WithdrawDataRequest.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class WithdrawDataRequest( + @Json(name = "amount_in_cents") val amountInCents: String, + @Json(name = "recipient_address") val recipientAddress: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/WithdrawRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/WithdrawRequest.kt new file mode 100644 index 0000000000..f9c0ff2dce --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/WithdrawRequest.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class WithdrawRequest( + @Json(name = "amount_in_cents") val amountInCents: String, + @Json(name = "recipient_address") val recipientAddress: String, + @Json(name = "admin_signature") val adminSignature: String, + @Json(name = "admin_salt") val adminSalt: String, + @Json(name = "sender_address") val senderAddress: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawDataResponse.kt new file mode 100644 index 0000000000..8bc4fcbf1e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawDataResponse.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class WithdrawDataResponse( + @Json(name = "result") val result: Result?, + @Json(name = "error") val error: String?, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "hash") val hash: String, + @Json(name = "salt") val salt: String, + @Json(name = "sender_address") val senderAddress: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawResponse.kt new file mode 100644 index 0000000000..2b347f27c0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawResponse.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class WithdrawResponse( + @Json(name = "result") val result: Result?, + @Json(name = "error") val error: String?, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "order_id") val orderId: String, + @Json(name = "status") val status: String, + @Json(name = "type") val type: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index fe34d263a9..2c128557a6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -145,6 +145,8 @@ object PreferencesKeys { intPreferencesKey(name = "tronNetworkFeeNotificationShowCount") } + val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrders") } + fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key") // endregion diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index 63f65c3ff3..f4cb2f2cb9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -24,5 +24,11 @@ interface TangemPayStorage { suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId) suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? + suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) + + suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String? + + suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) + suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 3d616fb516..79b1362cb7 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -32,6 +32,7 @@ You are archiving this account, but you can always get it back. Archiving... Account + Can’t edit the account Account saved Account for rewards Account #%s — used for address derivation. @@ -444,7 +445,7 @@ Check your internet connection or switch to a different network Terms of service Default Address - Legacy Address + Legacy %s Address Receive assets %s address Sending assets in other networks will result in permanent loss. @@ -1441,9 +1442,21 @@ Swap any asset in your portfolio for card Card details Unfreeze Card + Withdraw + Withdraw unavailable now + You can\'t initiate swap or new withdrawal till the current one is finished + Withdrawal in progress + Failed to issue card + A technical error has occurred, please try again by clicking the button below. + A technical error has occurred, please contact support. It usually takes up to 15 minutes Setting up your Tangem Card + Issuing your card + We’re getting your card ready. This may take a little time. Tangem Pay + We could not verify your profile. If you have any questions, please contact support. + Unfortunately, we couldn\'t verify your identity + KYC in progress View Status KYC in progress for Tangem Pay Get card @@ -1459,6 +1472,7 @@ We’re fixing a technical issue. Please try again later. Service temporarily unavailable The service is currently unreachable. Please try again later. + Sync needed Tangem Pay is temporarily unavailable Tangem Pay Use your card or ring to restore access to your payment account diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayTopUpDataFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPaySwapDataFactory.kt similarity index 95% rename from data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayTopUpDataFactory.kt rename to data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPaySwapDataFactory.kt index 086f18ce33..b7f0683742 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayTopUpDataFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPaySwapDataFactory.kt @@ -16,7 +16,7 @@ import com.tangem.domain.models.ReceiveAddressModel.NameService import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayTopUpData -import com.tangem.domain.pay.TangemPayTopUpDataFactory +import com.tangem.domain.pay.TangemPaySwapDataFactory import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject @@ -30,11 +30,11 @@ private const val TOKEN_NAME = "USDC" private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" private const val TOKEN_DECIMALS = 6 -internal class DefaultTangemPayTopUpDataFactory @Inject constructor( +internal class DefaultTangemPaySwapDataFactory @Inject constructor( @NetworkMoshi moshi: Moshi, private val tangemPayWalletsManager: TangemPayWalletsManager, excludedBlockchains: ExcludedBlockchains, -) : TangemPayTopUpDataFactory { +) : TangemPaySwapDataFactory { private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) { CryptoCurrencyFactory(excludedBlockchains) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt index 32c978f1b1..bdc2fdb4f5 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/datasource/DefaultTangemPayAuthDataSource.kt @@ -27,4 +27,13 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor( .mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") } .bind() } + + override suspend fun getWithdrawalSignature(cardId: String, hash: String): Either { + return when (val signResult = tangemSdkManager.getWithdrawalSignature(cardId, hash)) { + is CompletionResult.Failure<*> -> Either.Left( + IllegalStateException("TangemPay sign hash failed: ${signResult.error}"), + ) + is CompletionResult.Success -> Either.Right(signResult.data) + } + } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 57640bba9d..7ebe23b51d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -1,18 +1,15 @@ package com.tangem.data.pay.di -import com.tangem.data.pay.DefaultTangemPayTopUpDataFactory -import com.tangem.data.pay.repository.DefaultKycRepository -import com.tangem.data.pay.repository.DefaultOnboardingRepository -import com.tangem.data.pay.repository.DefaultTangemPayCardDetailsRepository -import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository +import com.tangem.data.pay.DefaultTangemPaySwapDataFactory +import com.tangem.data.pay.repository.* import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase -import com.tangem.domain.pay.TangemPayTopUpDataFactory -import com.tangem.domain.pay.repository.KycRepository -import com.tangem.domain.pay.repository.OnboardingRepository -import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase +import com.tangem.domain.pay.TangemPaySwapDataFactory +import com.tangem.domain.pay.repository.* import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase +import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository import dagger.Binds import dagger.Module @@ -43,7 +40,15 @@ internal interface TangemPayDataModule { @Binds @Singleton - fun bindDataForReceiveFactory(factory: DefaultTangemPayTopUpDataFactory): TangemPayTopUpDataFactory + fun bindTangemPaySwapRepository(repository: DefaultTangemPaySwapRepository): TangemPaySwapRepository + + @Binds + @Singleton + fun bindCustomerOrderRepository(repository: DefaultCustomerOrderRepository): CustomerOrderRepository + + @Binds + @Singleton + fun bindTangemPaySwapDataFactory(factory: DefaultTangemPaySwapDataFactory): TangemPaySwapDataFactory @Binds @Singleton @@ -51,13 +56,21 @@ internal interface TangemPayDataModule { impl: DefaultGetTangemPayCurrencyStatusUseCase, ): GetTangemPayCurrencyStatusUseCase + @Binds + @Singleton + fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase + companion object { @Provides @Singleton fun provideTangemPayMainScreenCustomerInfoUseCase( repository: OnboardingRepository, + customerOrderRepository: CustomerOrderRepository, ): TangemPayMainScreenCustomerInfoUseCase { - return TangemPayMainScreenCustomerInfoUseCase(repository = repository) + return TangemPayMainScreenCustomerInfoUseCase( + repository = repository, + customerOrderRepository = customerOrderRepository, + ) } @Provides diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt new file mode 100644 index 0000000000..11afd970e8 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt @@ -0,0 +1,46 @@ +package com.tangem.data.pay.repository + +import arrow.core.Either +import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.visa.error.VisaApiError +import javax.inject.Inject + +internal class DefaultCustomerOrderRepository @Inject constructor( + private val tangemPayApi: TangemPayApi, + private val requestHelper: TangemPayRequestPerformer, + private val tangemPayStorage: TangemPayStorage, +) : CustomerOrderRepository { + + override suspend fun getOrderStatus( + userWalletId: UserWalletId, + orderId: String, + ): Either { + return requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId) + }.map { response -> + when (response.result?.status) { + null -> OrderStatus.UNKNOWN + OrderStatus.NEW.apiName -> OrderStatus.NEW + OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING + OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED + else -> OrderStatus.CANCELED + } + } + } + + override suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean { + val orderId = tangemPayStorage.getWithdrawOrderId(userWalletId) + if (orderId == null) return false + + val status = getOrderStatus(userWalletId, orderId).getOrNull() + + val hasActiveOrder = status == OrderStatus.NEW || status == OrderStatus.PROCESSING + if (!hasActiveOrder) tangemPayStorage.deleteWithdrawOrder(userWalletId) + + return hasActiveOrder + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index c6fa9b8d4e..32806ec772 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -14,7 +14,6 @@ import com.tangem.domain.pay.datasource.TangemPayAuthDataSource import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.CustomerInfo.CardInfo import com.tangem.domain.pay.model.CustomerInfo.ProductInstance -import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayCardFrozenState @@ -170,23 +169,6 @@ internal class DefaultOnboardingRepository @Inject constructor( } } - override suspend fun getOrderStatus( - userWalletId: UserWalletId, - orderId: String, - ): Either { - return requestHelper.performRequest(userWalletId) { authHeader -> - tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId) - }.map { response -> - when (response.result?.status) { - null -> OrderStatus.UNKNOWN - OrderStatus.NEW.apiName -> OrderStatus.NEW - OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING - OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED - else -> OrderStatus.CANCELED - } - } - } - override suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either { val hasTangemPay = tangemPayStorage.checkCustomerWalletResult(userWalletId) if (hasTangemPay == true) { diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index b92ec16efe..12c3438e17 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -42,8 +42,12 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( tangemPayApi.getCardBalance(authHeader) }.result ?: error("Cannot get card balance") TangemPayCardBalance( - balance = result.fiat.availableBalance, + fiatBalance = result.fiat.availableBalance, currencyCode = result.fiat.currency, + cryptoBalance = result.crypto.balance, + chainId = result.crypto.chainId, + depositAddress = result.crypto.depositAddress, + contractAddress = result.crypto.tokenContractAddress, ) } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt new file mode 100644 index 0000000000..1b6c73d66d --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt @@ -0,0 +1,106 @@ +package com.tangem.data.pay.repository + +import arrow.core.Either +import com.tangem.core.error.UniversalError +import com.tangem.data.common.quote.QuotesFetcher +import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest +import com.tangem.datasource.api.pay.models.request.WithdrawRequest +import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.datasource.TangemPayAuthDataSource +import com.tangem.domain.pay.repository.TangemPaySwapRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.features.hotwallet.HotWalletFeatureToggles +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.Currency +import java.util.Locale +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class DefaultTangemPaySwapRepository @Inject constructor( + private val tangemPayApi: TangemPayApi, + private val requestHelper: TangemPayRequestPerformer, + private val authDataSource: TangemPayAuthDataSource, + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val quotesFetcher: QuotesFetcher, + private val tangemPayStorage: TangemPayStorage, +) : TangemPaySwapRepository { + + override suspend fun withdraw( + userWalletId: UserWalletId, + receiverAddress: String, + cryptoAmount: BigDecimal, + cryptoCurrencyId: CryptoCurrency.RawID, + ): Either { + val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId) + if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError) + return requestHelper.makeSafeRequest(userWalletId) { authHeader -> + val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress) + tangemPayApi.getWithdrawData(authHeader = authHeader, body = request) + }.map { data -> + val result = data.result + if (result == null) return Either.Left(VisaApiError.WithdrawalDataError) + val signature = authDataSource.getWithdrawalSignature(cardId = getCardId(userWalletId), hash = result.hash) + .getOrNull() + if (signature == null) return Either.Left(VisaApiError.SignWithdrawError) + + requestHelper.makeSafeRequest(userWalletId) { authHeader -> + val request = WithdrawRequest( + amountInCents = amountInCents, + recipientAddress = receiverAddress, + adminSalt = result.salt, + senderAddress = result.senderAddress, + adminSignature = signature, + ) + tangemPayApi.withdraw(authHeader = authHeader, body = request) + } + .mapLeft { return Either.Left(VisaApiError.WithdrawError) } + .map { response -> + val orderId = response.result?.orderId + if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWalletId, orderId) + } + } + } + + private suspend fun getAmountInCents(cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID): String? { + val fiatRate = getFiatRate(cryptoCurrencyId) ?: return null + val amountInDollars = cryptoAmount.multiply(fiatRate) + val defaultFractionDigits = Currency.getInstance(Locale.US).defaultFractionDigits + return amountInDollars + .setScale(defaultFractionDigits, RoundingMode.HALF_UP) + .movePointRight(defaultFractionDigits) + .longValueExact() + .toString() + } + + private suspend fun getFiatRate(cryptoCurrencyId: CryptoCurrency.RawID): BigDecimal? { + val quotes = quotesFetcher.fetch( + fiatCurrencyId = Currency.getInstance(Locale.US).currencyCode, + currencyId = cryptoCurrencyId.value, + field = QuotesFetcher.Field.PRICE, + ).getOrNull() + + return quotes?.quotes[cryptoCurrencyId.value]?.price + } + + private fun getCardId(userWalletId: UserWalletId): String { + val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId } + } else { + userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId } + } ?: error("No User Wallet found") + return if (userWallet is UserWallet.Cold) { + userWallet.cardId + } else { + TODO("[REDACTED_JIRA]") + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt new file mode 100644 index 0000000000..a18dfc3b7e --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt @@ -0,0 +1,29 @@ +package com.tangem.data.pay.usecase + +import arrow.core.Either +import com.tangem.core.error.UniversalError +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.repository.TangemPaySwapRepository +import com.tangem.domain.tangempay.TangemPayWithdrawUseCase +import java.math.BigDecimal +import javax.inject.Inject + +internal class DefaultTangemPayWithdrawUseCase @Inject constructor( + private val repository: TangemPaySwapRepository, +) : TangemPayWithdrawUseCase { + + override suspend fun invoke( + userWalletId: UserWalletId, + cryptoAmount: BigDecimal, + cryptoCurrencyId: CryptoCurrency.RawID, + receiverCexAddress: String, + ): Either { + return repository.withdraw( + userWalletId = userWalletId, + cryptoAmount = cryptoAmount, + receiverAddress = receiverCexAddress, + cryptoCurrencyId = cryptoCurrencyId, + ) + } +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt index c94c767e02..4321710d19 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt @@ -62,6 +62,9 @@ sealed class VisaApiError( data object RefreshTokenExpired : VisaApiError(104004001) data object NotPaeraCustomer : VisaApiError(104004002) + data object WithdrawalDataError : VisaApiError(104004003) + data object SignWithdrawError : VisaApiError(104004004) + data object WithdrawError : VisaApiError(104004005) companion object { fun fromBackendError(backendErrorCode: Int): VisaApiError { diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayTopUpDataFactory.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPaySwapDataFactory.kt similarity index 95% rename from domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayTopUpDataFactory.kt rename to domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPaySwapDataFactory.kt index 887df24ef0..fe6cb7af82 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayTopUpDataFactory.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPaySwapDataFactory.kt @@ -7,7 +7,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import java.math.BigDecimal -interface TangemPayTopUpDataFactory { +interface TangemPaySwapDataFactory { fun create( depositAddress: String, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt index 4b6c93492d..93748e8365 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/datasource/TangemPayAuthDataSource.kt @@ -9,4 +9,6 @@ interface TangemPayAuthDataSource { suspend fun produceInitialCredentials(cardId: String): Either suspend fun refreshAuthTokens(refreshToken: String): Either + + suspend fun getWithdrawalSignature(cardId: String, hash: String): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCardBalance.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCardBalance.kt index cb19b0a2cd..32d797f18d 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCardBalance.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCardBalance.kt @@ -3,6 +3,10 @@ package com.tangem.domain.pay.model import java.math.BigDecimal data class TangemPayCardBalance( - val balance: BigDecimal, + val fiatBalance: BigDecimal, val currencyCode: String, + val cryptoBalance: BigDecimal, + val chainId: Int, + val depositAddress: String, + val contractAddress: String, ) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt new file mode 100644 index 0000000000..9ae1fd414b --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.pay.repository + +import arrow.core.Either +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.visa.error.VisaApiError + +interface CustomerOrderRepository { + + suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either + + suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index 0010b99546..58046210b6 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -4,7 +4,6 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.CustomerInfo -import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.visa.error.VisaApiError import kotlinx.coroutines.Job @@ -21,8 +20,10 @@ interface OnboardingRepository { suspend fun createOrder(userWalletId: UserWalletId): Job suspend fun clearOrderId(userWalletId: UserWalletId) + suspend fun getOrderId(userWalletId: UserWalletId): String? - suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either + suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either + fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt new file mode 100644 index 0000000000..c0cbbac415 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.pay.repository + +import arrow.core.Either +import com.tangem.core.error.UniversalError +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import java.math.BigDecimal + +interface TangemPaySwapRepository { + + suspend fun withdraw( + userWalletId: UserWalletId, + receiverAddress: String, + cryptoAmount: BigDecimal, + cryptoCurrencyId: CryptoCurrency.RawID, + ): Either +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 1d3d10d652..18cfc5a0e2 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -9,6 +9,7 @@ import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.model.TangemPayCustomerInfoError +import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.visa.error.VisaApiError import timber.log.Timber @@ -21,6 +22,7 @@ private const val TAG = "TangemPayMainScreenCustomerInfoUseCase" */ class TangemPayMainScreenCustomerInfoUseCase( private val repository: OnboardingRepository, + private val customerOrderRepository: CustomerOrderRepository, ) { suspend operator fun invoke( @@ -74,7 +76,7 @@ class TangemPayMainScreenCustomerInfoUseCase( userWalletId: UserWalletId, orderId: String, ): Either { - return repository.getOrderStatus(userWalletId, orderId = orderId) + return customerOrderRepository.getOrderStatus(userWalletId, orderId = orderId) .fold( ifLeft = { error -> error.mapErrorForCustomer().left() diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt new file mode 100644 index 0000000000..a7d5c6d02c --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.tangempay + +import arrow.core.Either +import com.tangem.core.error.UniversalError +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import java.math.BigDecimal + +interface TangemPayWithdrawUseCase { + + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoAmount: BigDecimal, + cryptoCurrencyId: CryptoCurrency.RawID, + receiverCexAddress: String, + ): Either +} \ No newline at end of file diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt index 3f33c58822..2d0f54f7b2 100644 --- a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt @@ -20,6 +20,7 @@ interface SwapComponent : ComposableContentComponent { val cryptoAmount: BigDecimal, val fiatAmount: BigDecimal, val depositAddress: String, + val isWithdrawal: Boolean, ) } diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt index 6db16f52c8..2d46805dd4 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt @@ -1,7 +1,13 @@ package com.tangem.feature.swap.domain.models.ui +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.feature.swap.domain.models.ExpressDataError +import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.SwapProvider import java.math.BigDecimal sealed class SwapTransactionState { @@ -16,6 +22,31 @@ sealed class SwapTransactionState { val timestamp: Long, ) : SwapTransactionState() + data class TangemPayWithdrawalData( + val cryptoAmount: BigDecimal, + val cryptoCurrencyId: CryptoCurrency.RawID, + val cexAddress: String, + val fromAmount: String?, + val fromAmountValue: BigDecimal?, + val toAmount: String?, + val toAmountValue: BigDecimal?, + val storeData: StoreTransactionData, + ) : SwapTransactionState() { + + data class StoreTransactionData( + val currencyToSend: CryptoCurrencyStatus, + val currencyToGet: CryptoCurrencyStatus, + val fromAccount: Account.CryptoPortfolio?, + val toAccount: Account.CryptoPortfolio?, + val amount: SwapAmount, + val swapProvider: SwapProvider, + val swapDataModel: SwapDataModel, + val txExternalUrl: String?, + val txExternalId: String?, + val averageDuration: Int?, + ) + } + data object DemoMode : SwapTransactionState() sealed class Error : SwapTransactionState() { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 09dbef0286..6f4381b8a6 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -61,7 +61,8 @@ interface SwapInteractor { * @param currencyToSend [Currency] * @param currencyToGet [Currency] * @param amountToSwap amount to swap - * @param fee for tx + * @param fee for tx (can be null only for tangem pay withdrawal) + * @return [SwapTransactionState] */ @Suppress("LongParameterList") @@ -75,8 +76,9 @@ interface SwapInteractor { toAccount: Account.CryptoPortfolio?, amountToSwap: String, includeFeeInAmount: IncludeFeeInAmount, - fee: TxFee, + fee: TxFee?, expressOperationType: ExpressOperationType, + isTangemPayWithdrawal: Boolean, ): SwapTransactionState // suspend fun updateQuotesStateWithSelectedFee( @@ -115,6 +117,21 @@ interface SwapInteractor { fun getNativeToken(networkId: String): CryptoCurrency + @Suppress("LongParameterList") + suspend fun storeSwapTransaction( + currencyToSend: CryptoCurrencyStatus, + currencyToGet: CryptoCurrencyStatus, + fromAccount: Account.CryptoPortfolio?, + toAccount: Account.CryptoPortfolio?, + amount: SwapAmount, + swapProvider: SwapProvider, + swapDataModel: SwapDataModel, + timestamp: Long, + txExternalUrl: String? = null, + txExternalId: String? = null, + averageDuration: Int? = null, + ) + interface Factory { fun create(selectedWalletId: UserWalletId): SwapInteractor } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index cab225f602..a8f0efef14 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -709,6 +709,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( return result } + @Suppress("NullableToStringCall") override suspend fun onSwap( swapProvider: SwapProvider, swapData: SwapDataModel?, @@ -718,8 +719,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( toAccount: Account.CryptoPortfolio?, amountToSwap: String, includeFeeInAmount: IncludeFeeInAmount, - fee: TxFee, + fee: TxFee?, expressOperationType: ExpressOperationType, + isTangemPayWithdrawal: Boolean, ): SwapTransactionState { Timber.i( """ @@ -757,6 +759,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( txFee = fee, swapProvider = swapProvider, expressOperationType = expressOperationType, + isTangemPayWithdrawal = isTangemPayWithdrawal, ) } ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { @@ -773,6 +776,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( amountToSwap = amountToSwap, ) } else { + if (fee == null) return SwapTransactionState.Error.UnknownError onSwapDex( provider = swapProvider, networkId = currencyToSend.currency.network.backendId, @@ -978,16 +982,17 @@ internal class SwapInteractorImpl @AssistedInject constructor( ).getOrNull() ?: error("failed to create extras") } - @Suppress("LongMethod") + @Suppress("LongMethod", "CanBeNonNullable") private suspend fun onSwapCex( currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, fromAccount: Account.CryptoPortfolio?, toAccount: Account.CryptoPortfolio?, amount: SwapAmount, - txFee: TxFee, + txFee: TxFee?, swapProvider: SwapProvider, expressOperationType: ExpressOperationType, + isTangemPayWithdrawal: Boolean, ): SwapTransactionState { val exchangeData = repository.getExchangeData( userWallet = userWallet, @@ -1010,6 +1015,33 @@ internal class SwapInteractorImpl @AssistedInject constructor( val exchangeDataCex = exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError + if (isTangemPayWithdrawal) { + return SwapTransactionState.TangemPayWithdrawalData( + cryptoAmount = amount.value, + cryptoCurrencyId = requireNotNull(currencyToSend.currency.id.rawCurrencyId), + cexAddress = exchangeDataCex.txTo, + fromAmount = amount.toStringWithRightOffset(), + fromAmountValue = amount.value, + toAmount = amountFormatter.formatSwapAmountToUI( + exchangeData.toTokenAmount, + currencyToGet.currency.symbol, + ), + toAmountValue = exchangeData.toTokenAmount.value, + storeData = SwapTransactionState.TangemPayWithdrawalData.StoreTransactionData( + currencyToSend = currencyToSend, + currencyToGet = currencyToGet, + fromAccount = fromAccount, + toAccount = toAccount, + amount = amount, + swapProvider = swapProvider, + swapDataModel = exchangeData, + txExternalUrl = "", + txExternalId = exchangeDataCex.externalTxId, + averageDuration = null, + ), + ) + } + val userWallet = userWallet if (userWallet is UserWallet.Cold && isDemoCardUseCase(userWallet.scanResponse.card.cardId)) { return SwapTransactionState.Error.UnknownError @@ -1017,7 +1049,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val txData = createTransferTransactionUseCase( amount = amount.value.convertToSdkAmount(currencyToSend), - fee = txFee.fee, + fee = requireNotNull(txFee).fee, memo = exchangeDataCex.txExtraId, destination = exchangeDataCex.txTo, userWalletId = userWalletId, @@ -1087,7 +1119,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } - private suspend fun storeSwapTransaction( + override suspend fun storeSwapTransaction( currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, fromAccount: Account.CryptoPortfolio?, @@ -1096,9 +1128,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapProvider: SwapProvider, swapDataModel: SwapDataModel, timestamp: Long, - txExternalUrl: String? = null, - txExternalId: String? = null, - averageDuration: Int? = null, + txExternalUrl: String?, + txExternalId: String?, + averageDuration: Int?, ) { swapTransactionRepository.storeTransaction( userWalletId = userWalletId, diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index b1c706c900..9be6f2453e 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.common.routing) implementation(projects.common.ui) implementation(projects.core.decompose) // For Route supertype + implementation(projects.core.error) /** Domain modules **/ implementation(projects.domain.models) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 7619ebe375..f60408e0c3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -45,6 +45,7 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase +import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase @@ -115,6 +116,7 @@ internal class SwapModel @Inject constructor( private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val accountsFeatureToggles: AccountsFeatureToggles, private val getTangemPayCurrencyStatusUseCase: GetTangemPayCurrencyStatusUseCase, + private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase, ) : Model() { private val params = paramsContainer.require() @@ -555,6 +557,7 @@ internal class SwapModel @Inject constructor( selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, isReverseSwapPossible = isReverseSwapPossible(), needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + hideFee = tangemPayInput?.isWithdrawal == true, ) if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) { analyticsEventHandler.send( @@ -702,7 +705,7 @@ internal class SwapModel @Inject constructor( val fromCurrency = requireNotNull(dataState.fromCryptoCurrency) val fee = dataState.selectedFee - if (fee == null) { + if (fee == null && tangemPayInput?.isWithdrawal != true) { makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) return } @@ -719,13 +722,18 @@ internal class SwapModel @Inject constructor( includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount, fee = fee, expressOperationType = ExpressOperationType.SWAP, + isTangemPayWithdrawal = tangemPayInput?.isWithdrawal == true, ) - }.onSuccess { - when (it) { + }.onSuccess { swapTransactionState -> + when (swapTransactionState) { is SwapTransactionState.TxSent -> { + if (fee == null) { + makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + return@onSuccess + } sendSuccessSwapEvent(fromCurrency.currency, fee.feeType) val url = getExplorerTransactionUrlUseCase( - txHash = it.txHash, + txHash = swapTransactionState.txHash, networkId = fromCurrency.currency.network.id, ).getOrElse { Timber.i("tx hash explore not supported") @@ -735,11 +743,11 @@ internal class SwapModel @Inject constructor( updateWalletBalance() uiState = stateBuilder.createSuccessState( uiState = uiState, - swapTransactionState = it, + swapTransactionState = swapTransactionState, dataState = dataState, txUrl = url, onExploreClick = { - if (it.txHash.isNotEmpty()) { + if (swapTransactionState.txHash.isNotEmpty()) { urlOpener.openUrl(url) } analyticsEventHandler.send( @@ -747,7 +755,7 @@ internal class SwapModel @Inject constructor( ) }, onStatusClick = { - val txExternalUrl = it.txExternalUrl + val txExternalUrl = swapTransactionState.txExternalUrl if (!txExternalUrl.isNullOrBlank()) { urlOpener.openUrl(txExternalUrl) analyticsEventHandler.send( @@ -771,12 +779,15 @@ internal class SwapModel @Inject constructor( startLoadingQuotesFromLastState() uiState = stateBuilder.createErrorTransactionAlert( uiState = uiState, - error = it, + error = swapTransactionState, onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, onSupportClick = ::onFailedTxEmailClick, isReverseSwapPossible = isReverseSwapPossible(), ) } + is SwapTransactionState.TangemPayWithdrawalData -> { + processTangemPayWithdrawal(swapTransactionState = swapTransactionState) + } } }.onFailure { Timber.e(it) @@ -786,6 +797,40 @@ internal class SwapModel @Inject constructor( } } + private suspend fun processTangemPayWithdrawal(swapTransactionState: SwapTransactionState.TangemPayWithdrawalData) { + tangemPayWithdrawUseCase( + userWalletId = userWalletId, + cryptoAmount = swapTransactionState.cryptoAmount, + cryptoCurrencyId = swapTransactionState.cryptoCurrencyId, + receiverCexAddress = swapTransactionState.cexAddress, + ) + .onLeft { + startLoadingQuotesFromLastState() + makeDefaultAlert() + } + .onRight { + swapInteractor.storeSwapTransaction( + currencyToSend = swapTransactionState.storeData.currencyToSend, + currencyToGet = swapTransactionState.storeData.currencyToGet, + fromAccount = swapTransactionState.storeData.fromAccount, + toAccount = swapTransactionState.storeData.toAccount, + amount = swapTransactionState.storeData.amount, + swapProvider = swapTransactionState.storeData.swapProvider, + swapDataModel = swapTransactionState.storeData.swapDataModel, + txExternalUrl = "", + timestamp = System.currentTimeMillis(), + txExternalId = swapTransactionState.storeData.txExternalId, + averageDuration = null, + ) + uiState = stateBuilder.createTangemPayWithdrawalSuccessState( + uiState = uiState, + swapTransactionState = swapTransactionState, + dataState = dataState, + ) + swapRouter.openScreen(SwapNavScreen.Success) + } + } + private fun sendSuccessEvent() { val provider = dataState.selectedProvider ?: return val fee = dataState.selectedFee?.feeType ?: return @@ -804,6 +849,7 @@ internal class SwapModel @Inject constructor( ) } + @Suppress("LongMethod") private fun givePermissionsToSwap() { modelScope.launch(dispatchers.main) { runCatching { @@ -840,8 +886,8 @@ internal class SwapModel @Inject constructor( spenderAddress = approveDataModel.spenderAddress, ), ) - }.onSuccess { - when (it) { + }.onSuccess { swapTransactionState -> + when (swapTransactionState) { is SwapTransactionState.TxSent -> { sendApproveSuccessEvent(fromToken, feeForPermission.feeType, approveType) updateWalletBalance() @@ -852,7 +898,7 @@ internal class SwapModel @Inject constructor( is SwapTransactionState.Error -> { uiState = stateBuilder.createErrorTransactionAlert( uiState = uiState, - error = it, + error = swapTransactionState, onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, onSupportClick = ::onFailedTxEmailClick, isReverseSwapPossible = isReverseSwapPossible(), @@ -865,6 +911,9 @@ internal class SwapModel @Inject constructor( isReverseSwapPossible = isReverseSwapPossible(), ) } + is SwapTransactionState.TangemPayWithdrawalData -> { + processTangemPayWithdrawal(swapTransactionState = swapTransactionState) + } } }.onFailure { makeDefaultAlert() } }.onFailure { @@ -1475,7 +1524,17 @@ internal class SwapModel @Inject constructor( } else { groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value } ?.providers - }.orEmpty() + } + ?.filterForTangemPayWithdrawal() + .orEmpty() + } + + private fun List.filterForTangemPayWithdrawal(): List { + return if (tangemPayInput?.isWithdrawal == true) { + filter { it.type == ExchangeProviderType.CEX } + } else { + this + } } private fun Map.getLastLoadedSuccessStates(): SuccessLoadedSwapData { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 00e5e36c6d..9c60150644 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -92,12 +92,14 @@ internal class SwapNotificationsFactory( return updatedNotifications.toPersistentList() } + @Suppress("LongParameterList") fun getConfirmationStateNotifications( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, feeCryptoCurrencyStatus: CryptoCurrencyStatus?, selectedFeeType: FeeType, providerName: String, + hideFee: Boolean, ): ImmutableList { val warnings = buildList { maybeAddRentExemptionError(quoteModel) @@ -105,7 +107,7 @@ internal class SwapNotificationsFactory( maybeAddNeedReserveToCreateAccountWarning(quoteModel) maybeAddPermissionNeededWarning(quoteModel, fromToken, providerName) maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType) - maybeAddUnableCoverFeeWarning(quoteModel, fromToken) + maybeAddUnableCoverFeeWarning(quoteModel, fromToken, hideFee) maybeAddTransactionInProgressWarning(quoteModel) } return warnings.toPersistentList() @@ -284,7 +286,9 @@ internal class SwapNotificationsFactory( private fun MutableList.maybeAddUnableCoverFeeWarning( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, + hideFee: Boolean, ) { + if (hideFee) return val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && quoteModel.permissionState !is PermissionDataState.PermissionLoading && diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 53ea29f518..319e347f54 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -277,6 +277,7 @@ internal class StateBuilder( selectedFeeType: FeeType, isReverseSwapPossible: Boolean, needApplyFCARestrictions: Boolean, + hideFee: Boolean, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -286,8 +287,9 @@ internal class StateBuilder( feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, selectedFeeType = selectedFeeType, providerName = swapProvider.name, + hideFee = hideFee, ) - val feeState = createFeeState(quoteModel.txFee, selectedFeeType) + val feeState = if (hideFee) FeeItemState.Empty else createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus val isInsufficientFunds = isInsufficientFundsCondition(quoteModel) @@ -853,6 +855,45 @@ internal class StateBuilder( ) } + fun createTangemPayWithdrawalSuccessState( + uiState: SwapStateHolder, + swapTransactionState: SwapTransactionState.TangemPayWithdrawalData, + dataState: SwapProcessDataState, + ): SwapStateHolder { + val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) + val toCryptoCurrency = requireNotNull(dataState.toCryptoCurrency) + val fromAmount = swapTransactionState.fromAmountValue ?: BigDecimal.ZERO + val toAmount = swapTransactionState.toAmountValue ?: BigDecimal.ZERO + val providerState = uiState.providerState as ProviderState.Content + + val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount)) + val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount)) + + val shouldShowStatus = providerState.type == ExchangeProviderType.CEX.providerName + return uiState.copy( + successState = SwapSuccessStateHolder( + timestamp = System.currentTimeMillis(), + txUrl = "", + providerName = stringReference(providerState.name), + providerType = stringReference(providerState.type), + showStatusButton = shouldShowStatus, + providerIcon = providerState.iconUrl, + rate = providerState.subtitle, + fee = TextReference.EMPTY, + fromTitle = getFromCardAccountTitle(fromAccount = dataState.fromAccount), + toTitle = getToCardAccountTitle(toAccount = dataState.toAccount), + fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()), + toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()), + fromTokenFiatAmount = stringReference(fromFiatAmount), + toTokenFiatAmount = stringReference(toFiatAmount), + fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency), + toTokenIconState = iconStateConverter.convert(toCryptoCurrency), + onExploreButtonClick = {}, + onStatusButtonClick = {}, + ), + ) + } + fun createErrorTransactionAlert( uiState: SwapStateHolder, error: SwapTransactionState.Error, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsErrorType.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsErrorType.kt index fa1fe5d982..0cec097b4b 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsErrorType.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsErrorType.kt @@ -1,5 +1,5 @@ package com.tangem.features.tangempay.entity internal enum class TangemPayDetailsErrorType { - Receive, + Receive, Withdraw, WithdrawInProgress } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt index 1b882ec4a5..d1d7f674aa 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt @@ -73,9 +73,14 @@ internal class TangemPayDetailsStateFactory( actionButtons = persistentListOf( ActionButtonConfig( text = resourceReference(id = R.string.tangempay_card_details_add_funds), - iconResId = R.drawable.ic_arrow_down_24, + iconResId = R.drawable.ic_plus_24, onClick = intents::onClickAddFunds, ), + ActionButtonConfig( + text = resourceReference(id = R.string.tangempay_card_details_withdraw), + iconResId = R.drawable.ic_arrow_up_24, + onClick = intents::onClickWithdraw, + ), ), ), addToWalletBlockState = null, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt index 4caef18f17..eb7110543b 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.pay.TangemPayTopUpDataFactory +import com.tangem.domain.pay.TangemPaySwapDataFactory import com.tangem.features.tangempay.components.TangemPayAddFundsComponent import com.tangem.features.tangempay.entity.TangemPayAddFundsUM import com.tangem.features.tangempay.model.transformers.TangemPayAddFundsUMConverter @@ -16,7 +16,7 @@ import javax.inject.Inject internal class TangemPayAddFundsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val tangemPayTopUpDataFactory: TangemPayTopUpDataFactory, + private val tangemPaySwapDataFactory: TangemPaySwapDataFactory, ) : Model() { private val params = paramsContainer.require() @@ -24,7 +24,7 @@ internal class TangemPayAddFundsModel @Inject constructor( val uiState: TangemPayAddFundsUM = getInitialState() private fun getInitialState(): TangemPayAddFundsUM { - val data = tangemPayTopUpDataFactory.create( + val data = tangemPaySwapDataFactory.create( depositAddress = params.depositAddress, chainId = params.chainId, cryptoBalance = params.cryptoBalance, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index 1f9565d831..53c39bd25e 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -18,7 +18,9 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.pay.TangemPayTopUpData +import com.tangem.domain.pay.TangemPaySwapDataFactory import com.tangem.domain.pay.model.TangemPayCardBalance +import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.domain.visa.model.TangemPayTxHistoryItem @@ -64,6 +66,8 @@ internal class TangemPayDetailsModel @Inject constructor( private val uiMessageSender: UiMessageSender, private val cardDetailsEventListener: CardDetailsEventListener, private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener, + private val tangemPaySwapDataFactory: TangemPaySwapDataFactory, + private val orderRepository: CustomerOrderRepository, ) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener { private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() @@ -199,22 +203,63 @@ internal class TangemPayDetailsModel @Inject constructor( } override fun onClickAddFunds() { - if (params.config.depositAddress == null || balance == null) { + val currentBalance = balance + val depositAddress = params.config.depositAddress + if (currentBalance == null || depositAddress == null) { showBottomSheetError(TangemPayDetailsErrorType.Receive) } else { bottomSheetNavigation.activate( TangemPayDetailsNavigation.AddFunds( walletId = params.userWalletId, - fiatBalance = requireNotNull(balance).balance, - // Using the fiat balance as crypto balance. This will be changed when back returns crypto balance - cryptoBalance = requireNotNull(balance).balance, - depositAddress = requireNotNull(params.config.depositAddress), + fiatBalance = currentBalance.fiatBalance, + cryptoBalance = currentBalance.cryptoBalance, + depositAddress = depositAddress, chainId = params.config.chainId, ), ) } } + override fun onClickWithdraw() { + val currentBalance = balance + val depositAddress = params.config.depositAddress + if (currentBalance == null || depositAddress == null) { + showBottomSheetError(TangemPayDetailsErrorType.Withdraw) + } else { + modelScope.launch { + val hasActiveWithdrawal = orderRepository.hasWithdrawOrder(userWalletId = params.userWalletId) + if (hasActiveWithdrawal) { + showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress) + } else { + val data = tangemPaySwapDataFactory.create( + depositAddress = depositAddress, + chainId = params.config.chainId, + cryptoBalance = currentBalance.cryptoBalance, + fiatBalance = currentBalance.fiatBalance, + ).getOrNull() + if (data != null) { + router.push( + AppRoute.Swap( + currencyFrom = data.currency, + userWalletId = data.walletId, + isInitialReverseOrder = false, + screenSource = AnalyticsParam.ScreensSources.TangemPay.value, + tangemPayInput = AppRoute.Swap.TangemPayInput( + cryptoAmount = data.cryptoBalance, + fiatAmount = data.fiatBalance, + depositAddress = data.depositAddress, + isWithdrawal = true, + ), + ), + ) + } else { + showBottomSheetError(TangemPayDetailsErrorType.Withdraw) + } + } + } + } + } + private fun fetchBalance(): Job { return modelScope.launch { val result = try { @@ -294,6 +339,7 @@ internal class TangemPayDetailsModel @Inject constructor( cryptoAmount = data.cryptoBalance, fiatAmount = data.fiatBalance, depositAddress = data.depositAddress, + isWithdrawal = false, ), ), ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt index 90d3d95786..7817286e55 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt @@ -35,7 +35,7 @@ internal class DetailsBalanceTransformer( private fun getBalanceText(balance: TangemPayCardBalance): String { val currency = Currency.getInstance(balance.currencyCode) - return balance.balance.format { + return balance.fiatBalance.format { fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol) } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt index dbbe82010d..d4c2320e2e 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi internal interface TangemPayDetailIntents { fun onRefreshSwipe(refreshState: ShowRefreshState) fun onClickAddFunds() + fun onClickWithdraw() fun onClickChangePin() fun onClickFreezeCard() fun onClickUnfreezeCard() diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt index 94b52cde6c..6e70e5d9b0 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt @@ -25,6 +25,32 @@ internal object TangemPayMessagesFactory { onClick { closeBs() } } } + TangemPayDetailsErrorType.Withdraw -> bottomSheetMessage { + infoBlock { + icon(R.drawable.img_attention_20) { + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention + } + title = TextReference.Res(R.string.tangempay_card_details_withdraw_error_title) + body = TextReference.Res(R.string.tangempay_card_details_receive_error_description) + } + primaryButton { + text = resourceReference(R.string.common_got_it) + onClick { closeBs() } + } + } + TangemPayDetailsErrorType.WithdrawInProgress -> bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_clock_24) { + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Informative + } + title = TextReference.Res(R.string.tangempay_card_details_withdraw_in_progress_title) + body = TextReference.Res(R.string.tangempay_card_details_withdraw_in_progress_description) + } + secondaryButton { + text = resourceReference(R.string.common_got_it) + onClick { closeBs() } + } + } } } diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index 25bf3febfa..628ecd897d 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -160,5 +160,7 @@ interface TangemSdkManager { ): CompletionResult suspend fun tangemPayProduceInitialCredentials(cardId: String): CompletionResult + + suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult // endregion } \ No newline at end of file