Updated on 2026-08-14
This commit is contained in:
parent
84af4950a1
commit
ff2559df3e
27 changed files with 904 additions and 20 deletions
|
|
@ -20,6 +20,7 @@ import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
|
|||
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
||||
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
|
||||
import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler
|
||||
import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler
|
||||
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
|
||||
import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
|
||||
|
|
@ -56,6 +57,7 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
private val promoDeepLink: PromoDeeplinkHandler.Factory,
|
||||
private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory,
|
||||
private val marketsTokenExchangesDeepLink: MarketsTokenExchangesDeepLinkHandler.Factory,
|
||||
private val tangemPayMainDeepLink: TangemPayMainDeepLinkHandler.Factory,
|
||||
private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory,
|
||||
private val newsDeepLink: NewsDeepLinkHandler.Factory,
|
||||
private val earnDeepLink: EarnDeepLinkHandler.Factory,
|
||||
|
|
@ -129,6 +131,10 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
private fun handleHttpDeepLinks(deeplinkUri: Uri, coroutineScope: CoroutineScope) {
|
||||
if (deeplinkUri.host == DeepLinkRoute.PayApp.host) {
|
||||
when {
|
||||
deeplinkUri.path?.startsWith("/pay-app-main") == true -> {
|
||||
tangemPayMainDeepLink.create(coroutineScope, getQueryParams(deeplinkUri))
|
||||
return
|
||||
}
|
||||
deeplinkUri.path?.startsWith("/pay-app") == true -> {
|
||||
onboardVisaDeepLink.create(deeplinkUri)
|
||||
return
|
||||
|
|
@ -168,6 +174,7 @@ internal class DeepLinkFactory @Inject constructor(
|
|||
DeepLinkRoute.News.host -> newsDeepLink.create(queryParams)
|
||||
DeepLinkRoute.Earn.host -> earnDeepLink.create(queryParams)
|
||||
DeepLinkRoute.Yield.host -> yieldDeepLink.create(coroutineScope, queryParams)
|
||||
DeepLinkRoute.PayAppMain.host -> tangemPayMainDeepLink.create(coroutineScope, queryParams)
|
||||
else -> {
|
||||
TangemLogger.i(
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.tangem.features.onramp.deeplink.SwapDeepLinkHandler
|
|||
import com.tangem.features.send.v2.api.deeplink.SellRedirectDeepLinkHandler
|
||||
import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler
|
||||
import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler
|
||||
import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler
|
||||
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
|
||||
import com.tangem.features.wallet.deeplink.PromoDeeplinkHandler
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler
|
||||
|
|
@ -82,6 +83,10 @@ class DeepLinkFactoryTest {
|
|||
every { create(any()) } returns mockk()
|
||||
}
|
||||
|
||||
private val tangemPayMainDeepLink = mockk<TangemPayMainDeepLinkHandler.Factory>(relaxed = true) {
|
||||
every { create(any(), any()) } returns mockk()
|
||||
}
|
||||
|
||||
private val cardSdkProvider = mockk<CardSdkProvider>(relaxed = true) {
|
||||
every { sdk.uiVisibility() } returns MutableStateFlow(false)
|
||||
}
|
||||
|
|
@ -130,6 +135,7 @@ class DeepLinkFactoryTest {
|
|||
swapDeepLink = swapDeepLinkFactory,
|
||||
promoDeepLink = promoDeepLinkFactory,
|
||||
onboardVisaDeepLink = onboardVisaDeepLink,
|
||||
tangemPayMainDeepLink = tangemPayMainDeepLink,
|
||||
newsDetailsDeepLink = newsDeeplink,
|
||||
newsDeepLink = newsDeepLinkFactory,
|
||||
earnDeepLink = earnDeepLinkFactory,
|
||||
|
|
@ -358,6 +364,14 @@ class DeepLinkFactoryTest {
|
|||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify { promoDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
|
||||
|
||||
// Test TangemPay
|
||||
every { mockedUri.host } returns "pay-app-main"
|
||||
every { mockedUri.queryParameterNames } returns setOf("param")
|
||||
every { mockedUri.getQueryParameter("param") } returns "value"
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify { tangemPayMainDeepLink.create(eq(testScope), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -381,6 +395,7 @@ class DeepLinkFactoryTest {
|
|||
sellDeepLinkFactory.create()
|
||||
swapDeepLinkFactory.create()
|
||||
promoDeepLinkFactory.create(any(), any())
|
||||
tangemPayMainDeepLink.create(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ sealed class DeepLinkRoute {
|
|||
data object Yield : DeepLinkRoute() {
|
||||
override val host: String = "yield"
|
||||
}
|
||||
|
||||
data object PayAppMain : DeepLinkRoute() {
|
||||
override val host: String = "pay-app-main"
|
||||
}
|
||||
}
|
||||
|
||||
enum class DeepLinkScheme(val scheme: String) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ object DeeplinkConst {
|
|||
|
||||
const val TANGEM_SCHEME = "tangem"
|
||||
const val WALLET_ID_KEY = "user_wallet_id"
|
||||
const val CUSTOMER_WALLET_ID_KEY = "customer_wallet_id"
|
||||
const val CUSTOMER_ID_KEY = "customer_id"
|
||||
const val NETWORK_ID_KEY = "network_id"
|
||||
const val TYPE_KEY = "type"
|
||||
const val TOKEN_ID_KEY = "token_id"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.common.routing.deeplink
|
|||
import android.os.Bundle
|
||||
import com.tangem.common.routing.DeepLinkRoute
|
||||
import com.tangem.common.routing.DeepLinkScheme
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.CUSTOMER_WALLET_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.NAME_KEY
|
||||
|
|
@ -11,6 +12,7 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
|||
import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import com.tangem.domain.visa.model.TangemPayPushNotificationType
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
|
||||
|
|
@ -18,6 +20,7 @@ object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
|
|||
override fun convert(value: Map<String, String>): String? {
|
||||
return when {
|
||||
value[DEEPLINK_KEY] != null -> value[DEEPLINK_KEY]
|
||||
isTangemPayPushNotificationPayload(value) -> buildTangemPayNotificationDeeplink(value)
|
||||
isTangemPushNotificationPayload(value) -> buildNotificationDeeplink(value)
|
||||
else -> null
|
||||
}
|
||||
|
|
@ -70,4 +73,19 @@ object PayloadToDeeplinkConverter : Converter<Map<String, String>, String?> {
|
|||
payload.containsKey(TOKEN_ID_KEY) &&
|
||||
payload.containsKey(WALLET_ID_KEY)
|
||||
}
|
||||
|
||||
private fun isTangemPayPushNotificationPayload(payload: Map<String, String>): Boolean {
|
||||
return payload.containsKey(CUSTOMER_WALLET_ID_KEY) && payload[TYPE_KEY] in TangemPayPushNotificationType.all
|
||||
}
|
||||
|
||||
private fun buildTangemPayNotificationDeeplink(payload: Map<String, String>): String? {
|
||||
val walletId = payload[CUSTOMER_WALLET_ID_KEY]
|
||||
val type = payload[TYPE_KEY]
|
||||
if (walletId.isNullOrEmpty() || type.isNullOrEmpty()) return null
|
||||
|
||||
return DeepLinkBuilder().setScheme(DeepLinkScheme.Tangem.scheme).apply {
|
||||
setAction(DeepLinkRoute.PayAppMain.host)
|
||||
payload.forEach { (key, value) -> addQueryParam(key, value) }
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,14 @@ package com.tangem.common.routing.deeplink
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.CUSTOMER_WALLET_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
|
||||
import com.tangem.domain.visa.model.TangemPayPushNotificationType
|
||||
import org.junit.Test
|
||||
|
||||
internal class PayloadToDeeplinkConverterTest {
|
||||
|
|
@ -145,4 +148,88 @@ internal class PayloadToDeeplinkConverterTest {
|
|||
// THEN
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tangem pay card_ready push payload WHEN convert THEN should return pay-app-main deeplink`() {
|
||||
// GIVEN
|
||||
val payload = mapOf(
|
||||
TYPE_KEY to TangemPayPushNotificationType.CARD_READY.value,
|
||||
CUSTOMER_WALLET_ID_KEY to "wallet123",
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = PayloadToDeeplinkConverter.convert(payload)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
"tangem://pay-app-main?type=card_ready&customer_wallet_id=wallet123",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tangem pay transaction_spend push payload WHEN convert THEN should return pay-app-main deeplink with transaction_id`() {
|
||||
// GIVEN
|
||||
val payload = mapOf(
|
||||
TYPE_KEY to TangemPayPushNotificationType.TRANSACTION_SPEND.value,
|
||||
CUSTOMER_WALLET_ID_KEY to "wallet123",
|
||||
TRANSACTION_ID_KEY to "test456",
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = PayloadToDeeplinkConverter.convert(payload)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
"tangem://pay-app-main?type=transaction_spend&customer_wallet_id=wallet123&transaction_id=test456",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tangem pay top_up push payload WHEN convert THEN should return pay-app-main deeplink`() {
|
||||
// GIVEN
|
||||
val payload = mapOf(
|
||||
TYPE_KEY to TangemPayPushNotificationType.TOP_UP.value,
|
||||
CUSTOMER_WALLET_ID_KEY to "wallet123",
|
||||
TRANSACTION_ID_KEY to "test456",
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = PayloadToDeeplinkConverter.convert(payload)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
"tangem://pay-app-main?type=declined_top_up&customer_wallet_id=wallet123&transaction_id=test456",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tangem pay collateral push payload WHEN convert THEN should return pay-app-main deeplink`() {
|
||||
// GIVEN
|
||||
val payload = mapOf(
|
||||
TYPE_KEY to TangemPayPushNotificationType.COLLATERAL.value,
|
||||
CUSTOMER_WALLET_ID_KEY to "wallet123",
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = PayloadToDeeplinkConverter.convert(payload)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(
|
||||
"tangem://pay-app-main?type=collateral&customer_wallet_id=wallet123",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tangem pay push payload with missing customer_wallet_id WHEN convert THEN should return null`() {
|
||||
// GIVEN
|
||||
val payload = mapOf(
|
||||
TYPE_KEY to TangemPayPushNotificationType.CARD_READY.value,
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = PayloadToDeeplinkConverter.convert(payload)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.visa.utils
|
|||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayTxHistoryResponse
|
||||
import com.tangem.domain.pay.utils.TangemPayTxHistoryItemStatusConverter
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isPositive
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
enum class TangemPayPushNotificationType(val value: String) {
|
||||
CARD_READY("card_ready"),
|
||||
TRANSACTION_SPEND("transaction_spend"),
|
||||
TOP_UP("declined_top_up"),
|
||||
COLLATERAL("collateral"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
private val map = entries.associateBy { it.value }
|
||||
|
||||
val all: Set<String> = entries.map { it.value }.toSet()
|
||||
|
||||
fun fromValue(value: String): TangemPayPushNotificationType? = map[value]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.data.visa.utils
|
||||
package com.tangem.domain.pay.utils
|
||||
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal object TangemPayTxHistoryItemStatusConverter : Converter<String, TangemPayTxHistoryItem.Status> {
|
||||
object TangemPayTxHistoryItemStatusConverter : Converter<String, TangemPayTxHistoryItem.Status> {
|
||||
override fun convert(value: String): TangemPayTxHistoryItem.Status {
|
||||
return when (value.uppercase()) {
|
||||
"PENDING" -> TangemPayTxHistoryItem.Status.PENDING
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.tangempay.components
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
|
||||
interface TangemPayTransactionBottomSheetComponent : ComposableBottomSheetComponent {
|
||||
|
||||
data class Params(
|
||||
val isBalanceHidden: Boolean,
|
||||
val transaction: TangemPayTxHistoryItem,
|
||||
val userWalletId: UserWalletId,
|
||||
val customerId: String,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, TangemPayTransactionBottomSheetComponent>
|
||||
}
|
||||
|
|
@ -94,7 +94,7 @@ internal class TangemPayDetailsComponent(
|
|||
)
|
||||
is TangemPayDetailsNavigation.TransactionDetails -> TangemPayTxHistoryDetailsComponent(
|
||||
appComponentContext = context,
|
||||
params = TangemPayTxHistoryDetailsComponent.Params(
|
||||
params = TangemPayTransactionBottomSheetComponent.Params(
|
||||
transaction = navigation.transaction,
|
||||
isBalanceHidden = navigation.isBalanceHidden,
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
|
|
|
|||
|
|
@ -5,16 +5,17 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
|
||||
import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel
|
||||
import com.tangem.features.tangempay.ui.TangemPayTxHistoryDetailsContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class TangemPayTxHistoryDetailsComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
|
||||
internal class TangemPayTxHistoryDetailsComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: TangemPayTransactionBottomSheetComponent.Params,
|
||||
) : TangemPayTransactionBottomSheetComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: TangemPayTxHistoryDetailsModel = getOrCreateModel(params = params)
|
||||
|
||||
|
|
@ -28,11 +29,11 @@ internal class TangemPayTxHistoryDetailsComponent(
|
|||
TangemPayTxHistoryDetailsContent(state = state)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val transaction: TangemPayTxHistoryItem,
|
||||
val isBalanceHidden: Boolean,
|
||||
val userWalletId: UserWalletId,
|
||||
val customerId: String,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
@AssistedFactory
|
||||
interface Factory : TangemPayTransactionBottomSheetComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: TangemPayTransactionBottomSheetComponent.Params,
|
||||
): TangemPayTxHistoryDetailsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package com.tangem.features.tangempay.di
|
|||
|
||||
import com.tangem.features.tangempay.components.DefaultTangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
|
||||
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
|
||||
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
|
||||
import com.tangem.features.tangempay.model.listener.DefaultCardDetailsEventListener
|
||||
import dagger.Binds
|
||||
|
|
@ -23,4 +25,10 @@ internal interface TangemPayDetailsFeatureModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindCardDetailsEventListener(impl: DefaultCardDetailsEventListener): CardDetailsEventListener
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayTransactionBottomSheetComponentFactory(
|
||||
factory: TangemPayTxHistoryDetailsComponent.Factory,
|
||||
): TangemPayTransactionBottomSheetComponent.Factory
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
|||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -34,7 +34,7 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TangemPayTxHistoryDetailsComponent.Params>()
|
||||
private val params = paramsContainer.require<TangemPayTransactionBottomSheetComponent.Params>()
|
||||
val uiState: StateFlow<TangemPayTxHistoryDetailsUM>
|
||||
field = MutableStateFlow(
|
||||
value = TangemPayTxHistoryDetailsConverter.convert(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.tangempay.deeplink
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
interface TangemPayMainDeepLinkHandler {
|
||||
|
||||
interface Factory {
|
||||
fun create(scope: CoroutineScope, payload: Map<String, String>): TangemPayMainDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package com.tangem.features.tangempay.deeplink
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.CUSTOMER_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.CUSTOMER_WALLET_ID_KEY
|
||||
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.visa.model.TangemPayPushNotificationType
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultTangemPayMainDeepLinkHandler @AssistedInject constructor(
|
||||
@Assisted private val scope: CoroutineScope,
|
||||
@Assisted private val payload: Map<String, String>,
|
||||
private val appRouter: AppRouter,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val selectWalletUseCase: SelectWalletUseCase,
|
||||
private val walletDeepLinkActionTrigger: WalletDeepLinkActionTrigger,
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
private val paymentAccountSupplier: PaymentAccountStatusSupplier,
|
||||
) : TangemPayMainDeepLinkHandler {
|
||||
|
||||
init {
|
||||
handleDeepLink()
|
||||
}
|
||||
|
||||
private fun handleDeepLink() {
|
||||
val walletId = payload[CUSTOMER_WALLET_ID_KEY]
|
||||
|
||||
scope.launch {
|
||||
val userWalletId = walletId?.let(::UserWalletId) ?: run {
|
||||
appRouter.popTo(AppRoute.Wallet)
|
||||
return@launch
|
||||
}
|
||||
val userWallet = getUserWalletUseCase(userWalletId).getOrNull()
|
||||
if (userWallet == null || userWallet.isLocked) {
|
||||
appRouter.popTo(AppRoute.Wallet)
|
||||
return@launch
|
||||
}
|
||||
if (selectWalletUseCase(userWalletId).getOrNull() == null) {
|
||||
appRouter.popTo(AppRoute.Wallet)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val pushAction = buildPushAction()
|
||||
|
||||
appRouter.popTo(
|
||||
route = AppRoute.Wallet,
|
||||
onComplete = {
|
||||
walletDeepLinkActionTrigger.selectWallet(userWalletId)
|
||||
when (pushAction) {
|
||||
is TangemPayPushAction.CardReady,
|
||||
is TangemPayPushAction.TopUp,
|
||||
-> navigateToTangemPayDetails(userWalletId)
|
||||
is TangemPayPushAction.TransactionSpend -> {
|
||||
walletDeepLinkActionTrigger.showTangemPayTransaction(
|
||||
transaction = pushAction.transaction,
|
||||
customerId = pushAction.customerId,
|
||||
)
|
||||
}
|
||||
is TangemPayPushAction.CollateralTransaction -> {
|
||||
walletDeepLinkActionTrigger.showTangemPayTransaction(
|
||||
transaction = pushAction.transaction,
|
||||
customerId = pushAction.customerId,
|
||||
)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildPushAction(): TangemPayPushAction? {
|
||||
val type = payload[TYPE_KEY]?.let(TangemPayPushNotificationType::fromValue) ?: return null
|
||||
val customerId = payload[CUSTOMER_ID_KEY].orEmpty()
|
||||
|
||||
return when (type) {
|
||||
TangemPayPushNotificationType.CARD_READY -> TangemPayPushAction.CardReady
|
||||
TangemPayPushNotificationType.TRANSACTION_SPEND -> {
|
||||
val transaction = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
if (transaction != null) TangemPayPushAction.TransactionSpend(transaction, customerId) else null
|
||||
}
|
||||
TangemPayPushNotificationType.TOP_UP -> TangemPayPushAction.TopUp
|
||||
TangemPayPushNotificationType.COLLATERAL -> {
|
||||
val transaction = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
|
||||
if (transaction != null) TangemPayPushAction.CollateralTransaction(transaction, customerId) else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToTangemPayDetails(walletId: UserWalletId) {
|
||||
scope.launch {
|
||||
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(walletId))
|
||||
val paymentAccountStatus = paymentAccountSupplier.invoke(userWalletId = walletId)
|
||||
.firstOrNull()
|
||||
?: return@launch
|
||||
appRouter.push(route = AppRoute.TangemPayDetails(status = paymentAccountStatus))
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : TangemPayMainDeepLinkHandler.Factory {
|
||||
override fun create(scope: CoroutineScope, payload: Map<String, String>): DefaultTangemPayMainDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.tangempay.deeplink
|
||||
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
|
||||
internal sealed class TangemPayPushAction {
|
||||
|
||||
data object CardReady : TangemPayPushAction()
|
||||
|
||||
data class TransactionSpend(
|
||||
val transaction: TangemPayTxHistoryItem,
|
||||
val customerId: String,
|
||||
) : TangemPayPushAction()
|
||||
|
||||
data object TopUp : TangemPayPushAction()
|
||||
|
||||
data class CollateralTransaction(
|
||||
val transaction: TangemPayTxHistoryItem,
|
||||
val customerId: String,
|
||||
) : TangemPayPushAction()
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.tangem.features.tangempay.deeplink
|
||||
|
||||
import com.tangem.domain.pay.utils.TangemPayTxHistoryItemStatusConverter
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
object TangemPayPushPayloadToTxHistoryItemConverter {
|
||||
|
||||
private const val KEY_ID = "transaction_id"
|
||||
private const val KEY_AMOUNT = "amount"
|
||||
private const val KEY_CURRENCY = "currency"
|
||||
private const val KEY_LOCAL_AMOUNT = "local_amount"
|
||||
private const val KEY_LOCAL_CURRENCY = "local_currency"
|
||||
private const val KEY_AUTHORIZED_AMOUNT = "authorized_amount"
|
||||
private const val KEY_MERCHANT_NAME = "merchant_name"
|
||||
private const val KEY_ENRICHED_MERCHANT_NAME = "enriched_merchant_name"
|
||||
private const val KEY_ENRICHED_MERCHANT_ICON = "enriched_merchant_icon"
|
||||
private const val KEY_ENRICHED_MERCHANT_CATEGORY = "enriched_merchant_category"
|
||||
private const val KEY_MERCHANT_CATEGORY = "merchant_category"
|
||||
private const val KEY_MERCHANT_CATEGORY_CODE = "merchant_category_code"
|
||||
private const val KEY_STATUS = "status"
|
||||
private const val KEY_DECLINED_REASON = "declined_reason"
|
||||
private const val KEY_AUTHORIZED_AT = "authorized_at"
|
||||
private const val KEY_POSTED_AT = "posted_at"
|
||||
private const val KEY_TRANSACTION_HASH = "transaction_hash"
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
fun convertSpend(payload: Map<String, String>): TangemPayTxHistoryItem.Spend? {
|
||||
val id = payload[KEY_ID]?.ifEmpty { null } ?: return null
|
||||
val amount = payload[KEY_AMOUNT]?.toBigDecimalOrNull() ?: return null
|
||||
val currency = payload[KEY_CURRENCY]?.let(::parseCurrency) ?: return null
|
||||
val merchantName = payload[KEY_MERCHANT_NAME] ?: payload[KEY_ENRICHED_MERCHANT_NAME] ?: return null
|
||||
val status = payload[KEY_STATUS]?.ifEmpty { null } ?: return null
|
||||
val authorizedAt = payload[KEY_AUTHORIZED_AT]?.let(::parseDateTime) ?: return null
|
||||
|
||||
return TangemPayTxHistoryItem.Spend(
|
||||
id = id,
|
||||
jsonRepresentation = payload.toString(),
|
||||
date = authorizedAt.withZone(DateTimeZone.getDefault()),
|
||||
amount = amount,
|
||||
currency = currency,
|
||||
authorizedAmount = payload[KEY_AUTHORIZED_AMOUNT]?.toBigDecimalOrNull().orZero(),
|
||||
localAmount = payload[KEY_LOCAL_AMOUNT]?.toBigDecimalOrNull(),
|
||||
localCurrency = payload[KEY_LOCAL_CURRENCY]?.let(::parseCurrency),
|
||||
enrichedMerchantName = payload[KEY_ENRICHED_MERCHANT_NAME],
|
||||
merchantName = merchantName,
|
||||
enrichedMerchantCategory = payload[KEY_ENRICHED_MERCHANT_CATEGORY],
|
||||
merchantCategoryCode = payload[KEY_MERCHANT_CATEGORY_CODE],
|
||||
merchantCategory = payload[KEY_MERCHANT_CATEGORY],
|
||||
status = TangemPayTxHistoryItemStatusConverter.convert(status),
|
||||
enrichedMerchantIconUrl = payload[KEY_ENRICHED_MERCHANT_ICON],
|
||||
declinedReason = payload[KEY_DECLINED_REASON],
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
fun convertCollateral(payload: Map<String, String>): TangemPayTxHistoryItem.Collateral? {
|
||||
val id = payload[KEY_ID]?.ifEmpty { null } ?: return null
|
||||
val amount = payload[KEY_AMOUNT]?.toBigDecimalOrNull() ?: return null
|
||||
val transactionHash = payload[KEY_TRANSACTION_HASH]?.ifEmpty { null } ?: return null
|
||||
val postedAt = payload[KEY_POSTED_AT]?.let(::parseDateTime) ?: return null
|
||||
val currency = payload[KEY_CURRENCY]?.let(::parseCurrency) ?: return null
|
||||
|
||||
return TangemPayTxHistoryItem.Collateral(
|
||||
id = id,
|
||||
jsonRepresentation = payload.toString(),
|
||||
date = postedAt.withZone(DateTimeZone.getDefault()),
|
||||
currency = currency,
|
||||
amount = amount,
|
||||
transactionHash = transactionHash,
|
||||
type = if (amount >= BigDecimal.ZERO) {
|
||||
TangemPayTxHistoryItem.Type.Deposit
|
||||
} else {
|
||||
TangemPayTxHistoryItem.Type.Withdrawal
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseCurrency(code: String): Currency? = runCatching {
|
||||
return Currency.getInstance(code.uppercase())
|
||||
}.getOrNull()
|
||||
|
||||
private fun parseDateTime(value: String): DateTime? = runCatching {
|
||||
return DateTime.parse(value).withZone(DateTimeZone.getDefault())
|
||||
}.getOrNull()
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.features.tangempay.di
|
||||
|
||||
import com.tangem.features.tangempay.deeplink.DefaultOnboardVisaDeepLinkHandler
|
||||
import com.tangem.features.tangempay.deeplink.DefaultTangemPayMainDeepLinkHandler
|
||||
import com.tangem.features.tangempay.deeplink.OnboardVisaDeepLinkHandler
|
||||
import com.tangem.features.tangempay.deeplink.TangemPayMainDeepLinkHandler
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -15,4 +17,10 @@ internal interface TangemPayDeeplinkModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindDeepLinkHandlerFactory(impl: DefaultOnboardVisaDeepLinkHandler.Factory): OnboardVisaDeepLinkHandler.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayMainDeepLinkHandlerFactory(
|
||||
impl: DefaultTangemPayMainDeepLinkHandler.Factory,
|
||||
): TangemPayMainDeepLinkHandler.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
package com.tangem.features.tangempay.deeplink
|
||||
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
class TangemPayPushPayloadToTxHistoryItemConverterTest {
|
||||
|
||||
@Test
|
||||
fun `convertSpend returns Spend tx with all fields when payload is complete`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "txn-123",
|
||||
"amount" to "5.24",
|
||||
"currency" to "usd",
|
||||
"local_amount" to "5.24",
|
||||
"local_currency" to "usd",
|
||||
"authorized_amount" to "5.24",
|
||||
"merchant_name" to "PLAYSTATION NETWORK",
|
||||
"enriched_merchant_name" to "Playstation",
|
||||
"enriched_merchant_icon" to "https://example.com/icon.png",
|
||||
"enriched_merchant_category" to "Gaming",
|
||||
"merchant_category" to "Digital Goods",
|
||||
"merchant_category_code" to "5818",
|
||||
"status" to "completed",
|
||||
"declined_reason" to "",
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.id).isEqualTo("txn-123")
|
||||
assertThat(result.amount).isEqualTo(BigDecimal("5.24"))
|
||||
assertThat(result.currency.currencyCode).isEqualTo("USD")
|
||||
assertThat(result.localAmount).isEqualTo(BigDecimal("5.24"))
|
||||
assertThat(result.localCurrency?.currencyCode).isEqualTo("USD")
|
||||
assertThat(result.authorizedAmount).isEqualTo(BigDecimal("5.24"))
|
||||
assertThat(result.merchantName).isEqualTo("PLAYSTATION NETWORK")
|
||||
assertThat(result.enrichedMerchantName).isEqualTo("Playstation")
|
||||
assertThat(result.enrichedMerchantIconUrl).isEqualTo("https://example.com/icon.png")
|
||||
assertThat(result.enrichedMerchantCategory).isEqualTo("Gaming")
|
||||
assertThat(result.merchantCategory).isEqualTo("Digital Goods")
|
||||
assertThat(result.merchantCategoryCode).isEqualTo("5818")
|
||||
assertThat(result.status).isEqualTo(TangemPayTxHistoryItem.Status.COMPLETED)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend returns null when transaction_id is missing`() {
|
||||
val payload = mapOf(
|
||||
"amount" to "5.24",
|
||||
"currency" to "usd",
|
||||
"merchant_name" to "Test",
|
||||
"status" to "completed",
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend returns null when amount is missing`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "txn-123",
|
||||
"currency" to "usd",
|
||||
"merchant_name" to "Test",
|
||||
"status" to "completed",
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend returns null when currency is missing`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "txn-123",
|
||||
"amount" to "5.24",
|
||||
"merchant_name" to "Test",
|
||||
"status" to "completed",
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend returns null when merchant_name is missing`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "txn-123",
|
||||
"amount" to "5.24",
|
||||
"currency" to "usd",
|
||||
"status" to "completed",
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend falls back to enriched_merchant_name when merchant_name is absent`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "txn-123",
|
||||
"amount" to "5.24",
|
||||
"currency" to "usd",
|
||||
"enriched_merchant_name" to "Playstation",
|
||||
"status" to "completed",
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.merchantName).isEqualTo("Playstation")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend returns null when status is missing`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "txn-123",
|
||||
"amount" to "5.24",
|
||||
"currency" to "usd",
|
||||
"merchant_name" to "Test",
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend returns null when authorized_at is missing`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "txn-123",
|
||||
"amount" to "5.24",
|
||||
"currency" to "usd",
|
||||
"merchant_name" to "Test",
|
||||
"status" to "completed",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend returns null when amount is not a number`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "txn-123",
|
||||
"amount" to "abc",
|
||||
"currency" to "usd",
|
||||
"merchant_name" to "Test",
|
||||
"status" to "completed",
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend returns null when currency is invalid`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "txn-123",
|
||||
"amount" to "5.24",
|
||||
"currency" to "INVALID",
|
||||
"merchant_name" to "Test",
|
||||
"status" to "completed",
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend maps all statuses correctly`() {
|
||||
fun spendPayloadWithStatus(status: String) = mapOf(
|
||||
"transaction_id" to "txn-123",
|
||||
"amount" to "1.00",
|
||||
"currency" to "usd",
|
||||
"merchant_name" to "Test",
|
||||
"status" to status,
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("pending"))!!.status)
|
||||
.isEqualTo(TangemPayTxHistoryItem.Status.PENDING)
|
||||
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("reserved"))!!.status)
|
||||
.isEqualTo(TangemPayTxHistoryItem.Status.RESERVED)
|
||||
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("completed"))!!.status)
|
||||
.isEqualTo(TangemPayTxHistoryItem.Status.COMPLETED)
|
||||
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("declined"))!!.status)
|
||||
.isEqualTo(TangemPayTxHistoryItem.Status.DECLINED)
|
||||
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("reversed"))!!.status)
|
||||
.isEqualTo(TangemPayTxHistoryItem.Status.REVERSED)
|
||||
assertThat(TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(spendPayloadWithStatus("unknown_value"))!!.status)
|
||||
.isEqualTo(TangemPayTxHistoryItem.Status.UNKNOWN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend returns null when transaction_id is empty`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "",
|
||||
"amount" to "5.24",
|
||||
"currency" to "usd",
|
||||
"merchant_name" to "Test",
|
||||
"status" to "completed",
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertCollateral returns Collateral with all fields when payload is complete`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "col-123",
|
||||
"amount" to "50.00",
|
||||
"currency" to "usd",
|
||||
"transaction_hash" to "0xabc123",
|
||||
"posted_at" to "2025-10-25T19:22:22.597Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.id).isEqualTo("col-123")
|
||||
assertThat(result.amount).isEqualTo(BigDecimal("50.00"))
|
||||
assertThat(result.currency.currencyCode).isEqualTo("USD")
|
||||
assertThat(result.transactionHash).isEqualTo("0xabc123")
|
||||
assertThat(result.type).isEqualTo(TangemPayTxHistoryItem.Type.Deposit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertCollateral returns Withdrawal type for negative amount`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "col-456",
|
||||
"amount" to "-10.00",
|
||||
"currency" to "usd",
|
||||
"transaction_hash" to "0xdef789",
|
||||
"posted_at" to "2025-10-25T19:22:22.597Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.type).isEqualTo(TangemPayTxHistoryItem.Type.Withdrawal)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertCollateral returns null when transaction_id is missing`() {
|
||||
val payload = mapOf(
|
||||
"amount" to "50.00",
|
||||
"currency" to "usd",
|
||||
"transaction_hash" to "0xabc123",
|
||||
"posted_at" to "2025-10-25T19:22:22.597Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertCollateral returns null when amount is missing`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "col-123",
|
||||
"currency" to "usd",
|
||||
"transaction_hash" to "0xabc123",
|
||||
"posted_at" to "2025-10-25T19:22:22.597Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertCollateral returns null when transaction_hash is missing`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "col-123",
|
||||
"amount" to "50.00",
|
||||
"currency" to "usd",
|
||||
"posted_at" to "2025-10-25T19:22:22.597Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertCollateral returns null when posted_at is missing`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "col-123",
|
||||
"amount" to "50.00",
|
||||
"currency" to "usd",
|
||||
"transaction_hash" to "0xabc123",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertCollateral returns null when currency is missing`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "col-123",
|
||||
"amount" to "50.00",
|
||||
"transaction_hash" to "0xabc123",
|
||||
"posted_at" to "2025-10-25T19:22:22.597Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertCollateral returns null when transaction_hash is empty`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "col-123",
|
||||
"amount" to "50.00",
|
||||
"currency" to "usd",
|
||||
"transaction_hash" to "",
|
||||
"posted_at" to "2025-10-25T19:22:22.597Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend handles optional fields as null`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "txn-123",
|
||||
"amount" to "5.24",
|
||||
"currency" to "usd",
|
||||
"merchant_name" to "Test",
|
||||
"status" to "completed",
|
||||
"authorized_at" to "2025-10-24T10:32:24.496Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(payload)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.localAmount).isNull()
|
||||
assertThat(result.localCurrency).isNull()
|
||||
assertThat(result.enrichedMerchantName).isNull()
|
||||
assertThat(result.enrichedMerchantIconUrl).isNull()
|
||||
assertThat(result.enrichedMerchantCategory).isNull()
|
||||
assertThat(result.merchantCategory).isNull()
|
||||
assertThat(result.merchantCategoryCode).isNull()
|
||||
assertThat(result.declinedReason).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertCollateral returns Deposit type for zero amount`() {
|
||||
val payload = mapOf(
|
||||
"transaction_id" to "col-789",
|
||||
"amount" to "0",
|
||||
"currency" to "usd",
|
||||
"transaction_hash" to "0xdef",
|
||||
"posted_at" to "2025-10-25T19:22:22.597Z",
|
||||
)
|
||||
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(payload)
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result!!.type).isEqualTo(TangemPayTxHistoryItem.Type.Deposit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertSpend returns null for empty payload`() {
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertSpend(emptyMap())
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convertCollateral returns null for empty payload`() {
|
||||
val result = TangemPayPushPayloadToTxHistoryItemConverter.convertCollateral(emptyMap())
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ dependencies {
|
|||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.visa.models)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(tangemDeps.card.core)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
package com.tangem.features.wallet.deeplink
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WalletDeepLinkActionTrigger {
|
||||
fun selectWallet(userWalletId: UserWalletId)
|
||||
fun showTangemPayTransaction(transaction: TangemPayTxHistoryItem, customerId: String)
|
||||
}
|
||||
|
||||
interface WalletDeepLinkActionListener {
|
||||
val selectWalletFlow: Flow<UserWalletId>
|
||||
}
|
||||
val showTangemPayTransactionFlow: Flow<TangemPayTransactionDeepLinkData>
|
||||
}
|
||||
|
||||
data class TangemPayTransactionDeepLinkData(
|
||||
val transaction: TangemPayTxHistoryItem,
|
||||
val customerId: String,
|
||||
)
|
||||
|
|
@ -152,6 +152,7 @@ dependencies {
|
|||
implementation(projects.features.feed.api)
|
||||
implementation(projects.features.promoBanners.api)
|
||||
implementation(projects.features.tangempay.main.api)
|
||||
implementation(projects.features.tangempay.details.api)
|
||||
|
||||
/** Common modules */
|
||||
implementation(projects.common)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetCom
|
|||
import com.tangem.features.pushnotifications.api.PushNotificationsParams
|
||||
import com.tangem.features.send.v2.api.NetworkSelectionComponent
|
||||
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -56,6 +57,7 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
@Assisted navigate: (WalletRoute) -> Unit,
|
||||
feedEntryComponentFactory: FeedEntryComponent.Factory,
|
||||
tangemPayMainBlockComponentFactory: TangemPayMainBlockComponent.Factory,
|
||||
private val tangemPayTransactionBottomSheetComponentFactory: TangemPayTransactionBottomSheetComponent.Factory,
|
||||
private val renameWalletComponentFactory: RenameWalletComponent.Factory,
|
||||
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
|
||||
private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory,
|
||||
|
|
@ -242,6 +244,18 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
is WalletDialogConfig.TangemPayTransactionDetails -> {
|
||||
tangemPayTransactionBottomSheetComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TangemPayTransactionBottomSheetComponent.Params(
|
||||
isBalanceHidden = dialogConfig.isBalanceHidden,
|
||||
transaction = dialogConfig.transaction,
|
||||
userWalletId = dialogConfig.walletId,
|
||||
customerId = dialogConfig.customerId,
|
||||
onDismiss = model.innerWalletRouter.dialogNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ internal class WalletModel @Inject constructor(
|
|||
subscribeToScreenBackgroundState()
|
||||
subscribeOnPushNotificationsPermission()
|
||||
subscribeTangemPayOnWalletState()
|
||||
subscribeToTangemPayTransactionDeepLink()
|
||||
subscribeToMainScreenQrScanning()
|
||||
enableNotificationsIfNeeded()
|
||||
applyPendingAssetsDiscovery()
|
||||
|
|
@ -780,6 +781,21 @@ internal class WalletModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToTangemPayTransactionDeepLink() {
|
||||
walletDeepLinkActionListener.showTangemPayTransactionFlow
|
||||
.onEach { data ->
|
||||
innerWalletRouter.dialogNavigation.activate(
|
||||
WalletDialogConfig.TangemPayTransactionDetails(
|
||||
isBalanceHidden = stateHolder.value.isHidingMode,
|
||||
transaction = data.transaction,
|
||||
walletId = stateHolder.getSelectedWalletId(),
|
||||
customerId = data.customerId,
|
||||
),
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun handleQrResult(qrCode: String, resultSource: QrResultSource) {
|
||||
val target = resolveQrSendTargetsUseCase(qrCode)
|
||||
handleQrTarget(target, resultSource)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.feature.wallet.deeplink
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.features.wallet.deeplink.TangemPayTransactionDeepLinkData
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
|
@ -18,7 +20,15 @@ internal class DefaultWalletDeepLinkActionTrigger @Inject constructor() :
|
|||
override val selectWalletFlow: Flow<UserWalletId>
|
||||
get() = _selectWalletFlow.receiveAsFlow()
|
||||
|
||||
private val _showTangemPayTransactionFlow = Channel<TangemPayTransactionDeepLinkData>()
|
||||
override val showTangemPayTransactionFlow: Flow<TangemPayTransactionDeepLinkData>
|
||||
get() = _showTangemPayTransactionFlow.receiveAsFlow()
|
||||
|
||||
override fun selectWallet(userWalletId: UserWalletId) {
|
||||
_selectWalletFlow.trySend(userWalletId)
|
||||
}
|
||||
|
||||
override fun showTangemPayTransaction(transaction: TangemPayTxHistoryItem, customerId: String) {
|
||||
_showTangemPayTransactionFlow.trySend(TangemPayTransactionDeepLinkData(transaction, customerId))
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.serialization.BigDecimalSerializer
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.model.details.TokenAction
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -53,6 +54,14 @@ internal sealed interface WalletDialogConfig {
|
|||
@Serializable
|
||||
data class AddAndManage(val userWalletId: UserWalletId) : WalletDialogConfig
|
||||
|
||||
@Serializable
|
||||
data class TangemPayTransactionDetails(
|
||||
val isBalanceHidden: Boolean,
|
||||
val transaction: TangemPayTxHistoryItem,
|
||||
val walletId: UserWalletId,
|
||||
val customerId: String,
|
||||
) : WalletDialogConfig
|
||||
|
||||
@Serializable
|
||||
data class OrganizeTokens(val userWalletId: UserWalletId) : WalletDialogConfig
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue