Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-30 10:28:27 +02:00
parent 6a82ee1600
commit 229dc89bcb
28 changed files with 623 additions and 36 deletions

View file

@ -61,7 +61,7 @@ android {
}
flavorDimensions += "services"
productFlavors {
create("google") {
dimension = "services"
@ -73,6 +73,15 @@ android {
}
}
// `src/prodDi/` holds production DI bindings for interfaces with a `mocked` counterpart.
// Wired into every build type EXCEPT `mocked`, which supplies its own bindings from `src/mocked/`.
buildTypes.configureEach {
if (name != "mocked") {
sourceSets.named(name) {
java.srcDir("src/prodDi/java")
}
}
}
}
configurations.all {

View file

@ -1,9 +1,7 @@
package com.tangem.tap.di.data
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.tap.data.DefaultTangemPayStorage
import com.tangem.tap.data.DefaultVisaAuthTokenStorage
import com.tangem.tap.data.DefaultVisaOTPStorage
import dagger.Binds
@ -23,8 +21,4 @@ internal interface VisaStorageModule {
@Binds
@Singleton
fun bindVisaOTPStorage(impl: DefaultVisaOTPStorage): VisaOTPStorage
@Binds
@Singleton
fun bindTangemPayStorage(impl: DefaultTangemPayStorage): TangemPayStorage
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.core.security
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.security.DeviceSecurityInfoProvider
/** In MOCK env reports a clean device; otherwise delegates (DexProtector RTC flags emulators). */
internal class MockAwareDeviceSecurityInfoProvider(
private val real: DeviceSecurityInfoProvider,
private val apiConfigsManager: ApiConfigsManager,
) : DeviceSecurityInfoProvider {
private val isMockMode: Boolean
get() = apiConfigsManager
.getEnvironmentConfig(ApiConfig.ID.TangemPay)
.environment == ApiEnvironment.MOCK
override val isRooted: Boolean
get() = if (isMockMode) false else real.isRooted
override val isBootloaderUnlocked: Boolean
get() = if (isMockMode) false else real.isBootloaderUnlocked
override val isXposed: Boolean
get() = if (isMockMode) false else real.isXposed
override val isVulnerableToMediaTekExploit: Boolean
get() = if (isMockMode) false else real.isVulnerableToMediaTekExploit
}

View file

@ -0,0 +1,129 @@
package com.tangem.tap.data
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayWithdrawState
import com.tangem.domain.visa.model.TangemPayAuthTokens
import javax.inject.Inject
import javax.inject.Singleton
private const val MOCK_CUSTOMER_WALLET_ADDRESS = "0x0000000000000000000000000000000000000002"
private const val MOCK_ACCESS_TOKEN = "mock-access-token"
private const val MOCK_REFRESH_TOKEN = "mock-refresh-token"
private const val MOCK_IDEMPOTENCY_KEY = "mock-idempotency-key"
private const val MOCK_TOKEN_EXPIRES_AT = 9_999_999_999L
/** In MOCK env returns synthetic auth tokens + customer wallet address; otherwise delegates. */
@Singleton
internal class MockAwareTangemPayStorage @Inject constructor(
private val real: DefaultTangemPayStorage,
private val apiConfigsManager: ApiConfigsManager,
) : TangemPayStorage {
private val isMockMode: Boolean
get() = apiConfigsManager
.getEnvironmentConfig(ApiConfig.ID.TangemPay)
.environment == ApiEnvironment.MOCK
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
if (isMockMode) return
real.storeCustomerWalletAddress(userWalletId, customerWalletAddress)
}
override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? {
if (isMockMode) return MOCK_CUSTOMER_WALLET_ADDRESS
return real.getCustomerWalletAddress(userWalletId)
}
override suspend fun clearCustomerWalletAddress(userWalletId: UserWalletId) {
if (isMockMode) return
real.clearCustomerWalletAddress(userWalletId)
}
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) {
if (isMockMode) return
real.storeAuthTokens(customerWalletAddress, tokens)
}
override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? {
if (isMockMode) {
return TangemPayAuthTokens(
accessToken = MOCK_ACCESS_TOKEN,
expiresAt = MOCK_TOKEN_EXPIRES_AT,
refreshToken = MOCK_REFRESH_TOKEN,
refreshExpiresAt = MOCK_TOKEN_EXPIRES_AT,
idempotencyKey = MOCK_IDEMPOTENCY_KEY,
)
}
return real.getAuthTokens(customerWalletAddress)
}
override suspend fun clearAuthTokens(customerWalletAddress: String) {
if (isMockMode) return
real.clearAuthTokens(customerWalletAddress)
}
override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) =
real.storeOrderId(customerWalletAddress, orderId)
override suspend fun getOrderId(customerWalletAddress: String): String? =
real.getOrderId(customerWalletAddress)
override suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean =
real.getAddToWalletDone(customerWalletAddress)
override suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean) =
real.storeAddToWalletDone(customerWalletAddress, isDone)
override suspend fun clearOrderId(customerWalletAddress: String) =
real.clearOrderId(customerWalletAddress)
override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) =
real.storeCheckCustomerWalletResult(userWalletId, isPaeraCustomer)
override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? {
if (isMockMode) return true
return real.checkCustomerWalletResult(userWalletId)
}
override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) =
real.storeActiveWithdrawOrderId(userWalletId, orderId)
override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) =
real.storeWithdrawOrder(userWalletId, data)
override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? =
real.getActiveWithdrawOrderId(userWalletId)
override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<TangemPayWithdrawState>? =
real.getWithdrawOrders(userWalletId)
override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) =
real.deleteActiveWithdrawOrder(userWalletId)
override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) =
real.deleteWithdrawOrder(userWalletId, orderId)
override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) =
real.storeHideOnboardingBanner(userWalletId, hide)
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean =
real.getHideMainOnboardingBanner(userWalletId)
override suspend fun storeTangemPayEligibility(eligibility: Set<String>) =
real.storeTangemPayEligibility(eligibility)
override suspend fun getTangemPayEligibility(): Set<String> = real.getTangemPayEligibility()
override suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId) =
real.storeIsTangemPayDeactivated(userWalletId)
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean =
real.isTangemPayDeactivated(userWalletId)
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) =
real.clearAll(userWalletId, customerWalletAddress)
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.di.core.security
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.tap.core.security.DefaultDeviceSecurityInfoProvider
import com.tangem.tap.core.security.MockAwareDeviceSecurityInfoProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object SecurityMockedModule {
@Provides
@Singleton
fun provideDeviceSecurityInfoProvider(
apiConfigsManager: ApiConfigsManager,
): DeviceSecurityInfoProvider {
val real = DefaultDeviceSecurityInfoProvider()
return MockAwareDeviceSecurityInfoProvider(real = real, apiConfigsManager = apiConfigsManager)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di.data
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.tap.data.MockAwareTangemPayStorage
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface TangemPayStorageMockedModule {
@Binds
@Singleton
fun bindTangemPayStorage(impl: MockAwareTangemPayStorage): TangemPayStorage
}

View file

@ -10,7 +10,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object SecurityModule {
internal object SecurityProductionModule {
@Provides
@Singleton

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di.data
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.tap.data.DefaultTangemPayStorage
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface TangemPayStorageProductionModule {
@Binds
@Singleton
fun bindTangemPayStorage(impl: DefaultTangemPayStorage): TangemPayStorage
}

View file

@ -216,7 +216,15 @@ private fun ButtonsContainer(
} ?: TangemButtonIconPosition.None
TangemButton(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(
if (button.isPrimary) {
WarningBottomSheetTestTags.BUTTON_PRIMARY
} else {
WarningBottomSheetTestTags.BUTTON_SECONDARY
},
),
text = button.text?.resolveReference().orEmpty(),
icon = icon,
onClick = { button.onClick?.invoke(closeScope) },

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui.test
object HotWalletAccessCodeTestTags {
const val ACCESS_CODE_INPUT = "HOT_WALLET_ACCESS_CODE_INPUT"
}

View file

@ -0,0 +1,39 @@
package com.tangem.core.ui.test
object TangemPayTestTags {
// Main wallet screen tile (entry point into Tangem Pay)
const val MAIN_SCREEN_TILE = "TANGEM_PAY_MAIN_SCREEN_TILE"
// Payment account details screen
const val PAYMENT_ACCOUNT_BALANCE = "TANGEM_PAY_PAYMENT_ACCOUNT_BALANCE"
const val PAYMENT_ACCOUNT_CARD_BUTTON = "TANGEM_PAY_PAYMENT_ACCOUNT_CARD_BUTTON"
// Card details (reveal + copy)
const val CARD_DETAILS_SHOW_BUTTON = "TANGEM_PAY_CARD_DETAILS_SHOW_BUTTON"
const val CARD_DETAILS_HIDE_BUTTON = "TANGEM_PAY_CARD_DETAILS_HIDE_BUTTON"
const val CARD_DETAILS_NUMBER_VALUE = "TANGEM_PAY_CARD_DETAILS_NUMBER_VALUE"
const val CARD_DETAILS_EXPIRATION_VALUE = "TANGEM_PAY_CARD_DETAILS_EXPIRATION_VALUE"
const val CARD_DETAILS_CVC_VALUE = "TANGEM_PAY_CARD_DETAILS_CVC_VALUE"
const val CARD_DETAILS_COPY_NUMBER = "TANGEM_PAY_CARD_DETAILS_COPY_NUMBER"
const val CARD_DETAILS_COPY_EXPIRATION = "TANGEM_PAY_CARD_DETAILS_COPY_EXPIRATION"
const val CARD_DETAILS_COPY_CVC = "TANGEM_PAY_CARD_DETAILS_COPY_CVC"
// Card management (card page settings)
const val CHANGE_PIN_ROW = "TANGEM_PAY_CHANGE_PIN_ROW"
const val FREEZE_CARD_ROW = "TANGEM_PAY_FREEZE_CARD_ROW"
// Freeze confirmation bottom sheet
const val FREEZE_CONFIRMATION_SUBMIT_BUTTON = "TANGEM_PAY_FREEZE_CONFIRMATION_SUBMIT_BUTTON"
// PIN entry screen
const val PIN_SCREEN_TITLE = "TANGEM_PAY_PIN_SCREEN_TITLE"
const val PIN_SCREEN_DESCRIPTION = "TANGEM_PAY_PIN_SCREEN_DESCRIPTION"
const val PIN_INPUT_FIELD = "TANGEM_PAY_PIN_INPUT_FIELD"
const val PIN_SUBMIT_BUTTON = "TANGEM_PAY_PIN_SUBMIT_BUTTON"
const val PIN_ERROR_MESSAGE = "TANGEM_PAY_PIN_ERROR_MESSAGE"
// PIN success screen
const val PIN_SUCCESS_TITLE = "TANGEM_PAY_PIN_SUCCESS_TITLE"
const val PIN_SUCCESS_DESCRIPTION = "TANGEM_PAY_PIN_SUCCESS_DESCRIPTION"
const val PIN_DONE_BUTTON = "TANGEM_PAY_PIN_DONE_BUTTON"
}

View file

@ -4,4 +4,6 @@ object WarningBottomSheetTestTags {
const val ICON = "BASE_WARNING_BOTTOM_SHEET_ICON"
const val TITLE = "BASE_WARNING_BOTTOM_SHEET_TITLE"
const val MESSAGE = "BASE_WARNING_BOTTOM_SHEET_MESSAGE"
const val BUTTON_PRIMARY = "BASE_WARNING_BOTTOM_SHEET_BUTTON_PRIMARY"
const val BUTTON_SECONDARY = "BASE_WARNING_BOTTOM_SHEET_BUTTON_SECONDARY"
}

View file

@ -10,6 +10,16 @@ plugins {
android {
namespace = "com.tangem.data.visa"
// `src/prodDi/` holds production DI bindings for TangemPay repos with a `mocked` counterpart.
// Wired into every build type EXCEPT `mocked`, which supplies its own bindings from `src/mocked/`.
buildTypes.configureEach {
if (name != "mocked") {
sourceSets.named(name) {
java.srcDir("src/prodDi/kotlin")
}
}
}
}
tasks.withType<Test>().configureEach {

View file

@ -51,18 +51,10 @@ internal interface TangemPayDataModule {
@Singleton
fun bindKycRepository(repository: DefaultKycRepository): KycRepository
@Binds
@Singleton
fun bindOnboardingRepository(repository: DefaultOnboardingRepository): OnboardingRepository
@Binds
@Singleton
fun bindTangemPayTxHistoryRepository(repository: DefaultTangemPayTxHistoryRepository): TangemPayTxHistoryRepository
@Binds
@Singleton
fun bindCardDetailsRepository(repository: DefaultTangemPayCardDetailsRepository): TangemPayCardDetailsRepository
@Binds
@Singleton
fun bindTangemPaySwapRepository(repository: DefaultTangemPayWithdrawRepository): TangemPayWithdrawRepository

View file

@ -0,0 +1,24 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.repository.MockAwareOnboardingRepository
import com.tangem.data.pay.repository.MockAwareTangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface TangemPayDataMockedModule {
@Binds
@Singleton
fun bindOnboardingRepository(repository: MockAwareOnboardingRepository): OnboardingRepository
@Binds
@Singleton
fun bindCardDetailsRepository(repository: MockAwareTangemPayCardDetailsRepository): TangemPayCardDetailsRepository
}

View file

@ -0,0 +1,110 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.right
import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.domain.models.pay.TangemPayEligibilityType
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.CustomerInfo
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.visa.error.VisaApiError
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton
/** In MOCK env skips local-storage / signing enrollment; server calls go to WireMock. */
@Singleton
internal class MockAwareOnboardingRepository @Inject constructor(
private val real: DefaultOnboardingRepository,
private val apiConfigsManager: ApiConfigsManager,
) : OnboardingRepository {
private val mockOrderIds: MutableSet<UserWalletId> = ConcurrentHashMap.newKeySet()
private val isMockMode: Boolean
get() = apiConfigsManager
.getEnvironmentConfig(ApiConfig.ID.TangemPay)
.environment == ApiEnvironment.MOCK
override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> {
if (isMockMode) return true.right()
return real.validateDeeplink(link)
}
override suspend fun isTangemPayInitialDataProduced(userWalletId: UserWalletId): Boolean {
if (isMockMode) return true
return real.isTangemPayInitialDataProduced(userWalletId)
}
override suspend fun produceInitialData(userWalletId: UserWalletId) {
if (isMockMode) return
real.produceInitialData(userWalletId)
}
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> =
real.getCustomerInfo(userWalletId)
override suspend fun createOrder(userWalletId: UserWalletId): Either<VisaApiError, String> {
if (isMockMode) {
mockOrderIds.add(userWalletId)
return MOCK_ORDER_ID.right()
}
return real.createOrder(userWalletId)
}
override suspend fun clearOrderId(userWalletId: UserWalletId) {
if (isMockMode) {
mockOrderIds.remove(userWalletId)
return
}
real.clearOrderId(userWalletId)
}
override suspend fun getOrderId(userWalletId: UserWalletId): String? {
if (isMockMode) return MOCK_ORDER_ID.takeIf { userWalletId in mockOrderIds }
return real.getOrderId(userWalletId)
}
override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> =
real.hasTangemPayInWallet(userWalletId)
override suspend fun checkCustomerEligibility(): List<TangemPayEligibilityType> {
if (isMockMode) return listOf(TangemPayEligibilityType.DETAILS)
return real.checkCustomerEligibility()
}
override suspend fun getCustomerEligibility(): List<TangemPayEligibilityType> {
if (isMockMode) return listOf(TangemPayEligibilityType.DETAILS)
return real.getCustomerEligibility()
}
override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? =
real.getSavedCustomerInfo(userWalletId)
override suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean {
if (isMockMode) return false
return real.getHideMainOnboardingBanner(userWalletId)
}
override suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) {
if (isMockMode) return
real.setHideMainOnboardingBanner(userWalletId)
}
override suspend fun disableTangemPay(userWalletId: UserWalletId): Either<VisaApiError, Unit> {
if (isMockMode) return Unit.right()
return real.disableTangemPay(userWalletId)
}
override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean {
if (isMockMode) return false
return real.isTangemPayDeactivated(userWalletId)
}
private companion object {
const val MOCK_ORDER_ID = "mock-order-id"
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.right
import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.SetPinResult
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayCardDetails
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Singleton
/** In MOCK env short-circuits RSA-encrypted flows (reveal/getPin/setPin) with hardcoded values. */
@Singleton
internal class MockAwareTangemPayCardDetailsRepository @Inject constructor(
private val real: DefaultTangemPayCardDetailsRepository,
private val apiConfigsManager: ApiConfigsManager,
) : TangemPayCardDetailsRepository {
private val isMockMode: Boolean
get() = apiConfigsManager
.getEnvironmentConfig(ApiConfig.ID.TangemPay)
.environment == ApiEnvironment.MOCK
override suspend fun getCardBalance(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardBalance> =
real.getCardBalance(userWalletId)
override suspend fun revealCardDetails(
userWalletId: UserWalletId,
): Either<UniversalError, TangemPayCardDetails> {
if (isMockMode) {
return TangemPayCardDetails(
pan = MOCK_PAN,
cvv = MOCK_CVV,
expirationYear = MOCK_EXPIRATION_YEAR,
expirationMonth = MOCK_EXPIRATION_MONTH,
).right()
}
return real.revealCardDetails(userWalletId)
}
override suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?> {
if (isMockMode) return MOCK_PIN.right()
return real.getPin(userWalletId, cardId)
}
override suspend fun setPin(userWalletId: UserWalletId, pin: String): Either<UniversalError, SetPinResult> {
if (isMockMode) return SetPinResult.SUCCESS.right()
return real.setPin(userWalletId, pin)
}
override suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either<UniversalError, Boolean> =
real.isAddToWalletDone(userWalletId)
override suspend fun setAddToWalletAsDone(userWalletId: UserWalletId): Either<UniversalError, Unit> =
real.setAddToWalletAsDone(userWalletId)
override suspend fun freezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> = real.freezeCard(userWalletId, cardId)
override suspend fun unfreezeCard(
userWalletId: UserWalletId,
cardId: String,
): Either<UniversalError, TangemPayCardFrozenState> = real.unfreezeCard(userWalletId, cardId)
override fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState> =
real.cardFrozenState(cardId)
override suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState? =
real.cardFrozenStateSync(cardId)
override suspend fun updateCardDisplayName(
cardId: String,
userWalletId: UserWalletId,
displayName: CardDisplayName,
): Either<UniversalError, Unit> = real.updateCardDisplayName(cardId, userWalletId, displayName)
override suspend fun updateCardLimit(
cardId: String,
userWalletId: UserWalletId,
limit: String,
): Either<UniversalError, Unit> = real.updateCardLimit(cardId, userWalletId, limit)
private companion object {
const val MOCK_PAN = "4242 4242 4242 4242"
const val MOCK_CVV = "123"
const val MOCK_EXPIRATION_YEAR = "2028"
const val MOCK_EXPIRATION_MONTH = "12"
const val MOCK_PIN = "1234"
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.repository.DefaultOnboardingRepository
import com.tangem.data.pay.repository.DefaultTangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface TangemPayDataProductionModule {
@Binds
@Singleton
fun bindOnboardingRepository(repository: DefaultOnboardingRepository): OnboardingRepository
@Binds
@Singleton
fun bindCardDetailsRepository(repository: DefaultTangemPayCardDetailsRepository): TangemPayCardDetailsRepository
}

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -22,6 +23,7 @@ import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.HotWalletAccessCodeTestTags
import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
@ -109,6 +111,7 @@ internal fun AccessCode(
pinTextColor = state.accessCodeColor,
onValueChange = state.onAccessCodeChange,
focusRequester = focusRequester,
modifier = Modifier.testTag(HotWalletAccessCodeTestTags.ACCESS_CODE_INPUT),
)
}
}

View file

@ -36,5 +36,6 @@ internal data class TangemPayCardPageUM(
@Immutable
internal data class TangemPayCardPageSetting(
val title: TextReference,
val testTag: String? = null,
val onSettingClick: () -> Unit,
)

View file

@ -117,10 +117,12 @@ internal class TangemPayCardPageModel @Inject constructor(
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_change_pin),
onSettingClick = { onClickChangePIN(card.hasPinCode) },
testTag = com.tangem.core.ui.test.TangemPayTestTags.CHANGE_PIN_ROW,
),
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_freeze_card),
onSettingClick = { onClickFreezeOrUnfreezeCard(card.isFrozen) },
testTag = com.tangem.core.ui.test.TangemPayTestTags.FREEZE_CARD_ROW,
),
TangemPayCardPageSetting(
title = TextReference.Res(R.string.tangempay_card_details_reissue_card),

View file

@ -29,6 +29,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
@ -49,6 +50,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.domain.models.account.CardDisplayName
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.features.tangempay.details.impl.R
@ -209,10 +211,12 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
}
TangemPayCardDetailsCustomButton(
modifier = Modifier.constrainAs(buttonRef) {
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
},
modifier = Modifier
.constrainAs(buttonRef) {
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
.testTag(TangemPayTestTags.CARD_DETAILS_SHOW_BUTTON),
text = stringResourceSafe(id = R.string.tangempay_card_details_show_details),
onClick = state.onClick,
showProgress = state.isLoading,
@ -336,6 +340,8 @@ private fun TangemPayCardDetailsShownBlock(
title = stringResourceSafe(R.string.tangempay_card_details_card_number),
text = cardNumber,
onCopy = onCopyCardNumber,
valueTestTag = TangemPayTestTags.CARD_DETAILS_NUMBER_VALUE,
copyTestTag = TangemPayTestTags.CARD_DETAILS_COPY_NUMBER,
)
Row(
modifier = Modifier
@ -350,6 +356,8 @@ private fun TangemPayCardDetailsShownBlock(
title = stringResourceSafe(R.string.tangempay_card_details_expiry),
text = expiry,
onCopy = onCopyExpiry,
valueTestTag = TangemPayTestTags.CARD_DETAILS_EXPIRATION_VALUE,
copyTestTag = TangemPayTestTags.CARD_DETAILS_COPY_EXPIRATION,
)
CardDetailsTextContainer(
modifier = Modifier
@ -358,13 +366,17 @@ private fun TangemPayCardDetailsShownBlock(
title = stringResourceSafe(R.string.tangempay_card_details_cvc),
text = cvv,
onCopy = onCopyCvv,
valueTestTag = TangemPayTestTags.CARD_DETAILS_CVC_VALUE,
copyTestTag = TangemPayTestTags.CARD_DETAILS_COPY_CVC,
)
}
Spacer(modifier = Modifier.weight(1f))
Row {
SpacerWMax()
TangemPayCardDetailsCustomButton(
modifier = Modifier.padding(end = 16.dp, bottom = 8.dp),
modifier = Modifier
.padding(end = 16.dp, bottom = 8.dp)
.testTag(TangemPayTestTags.CARD_DETAILS_HIDE_BUTTON),
text = stringResourceSafe(id = R.string.tangempay_card_details_hide_details),
onClick = onHideDetails,
showProgress = false,
@ -374,7 +386,14 @@ private fun TangemPayCardDetailsShownBlock(
}
@Composable
private fun CardDetailsTextContainer(title: String, text: String, onCopy: () -> Unit, modifier: Modifier = Modifier) {
private fun CardDetailsTextContainer(
title: String,
text: String,
onCopy: () -> Unit,
modifier: Modifier = Modifier,
valueTestTag: String? = null,
copyTestTag: String? = null,
) {
Row(
modifier = modifier
.background(
@ -395,10 +414,13 @@ private fun CardDetailsTextContainer(title: String, text: String, onCopy: () ->
text = text,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.constantWhite,
modifier = if (valueTestTag != null) Modifier.testTag(valueTestTag) else Modifier,
)
}
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
modifier = Modifier
.size(TangemTheme.dimens.size32)
.then(if (copyTestTag != null) Modifier.testTag(copyTestTag) else Modifier),
onClick = onCopy,
) {
Icon(

View file

@ -31,6 +31,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
@ -169,7 +170,8 @@ private fun TangemPayCardPageSettingRow(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(TangemTheme.dimens.spacing12),
.padding(TangemTheme.dimens.spacing12)
.then(if (item.testTag != null) Modifier.testTag(item.testTag) else Modifier),
contentAlignment = Alignment.CenterStart,
) {
Text(

View file

@ -8,6 +8,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -23,6 +24,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.features.tangempay.details.impl.R
@Composable
@ -43,7 +45,7 @@ internal fun TangemPayChangePinCodeSuccessScreen(onClick: () -> Unit, modifier:
titleAlignment = Alignment.CenterHorizontally,
)
Column(
modifier
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
@ -58,7 +60,8 @@ internal fun TangemPayChangePinCodeSuccessScreen(onClick: () -> Unit, modifier:
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
.navigationBarsPadding(),
.navigationBarsPadding()
.testTag(TangemPayTestTags.PIN_DONE_BUTTON),
text = stringResourceSafe(R.string.common_done),
onClick = onClick,
)
@ -101,14 +104,18 @@ private fun SuccessContent(modifier: Modifier = Modifier) {
)
SpacerH32()
Text(
modifier = Modifier.padding(horizontal = 32.dp),
modifier = Modifier
.padding(horizontal = 32.dp)
.testTag(TangemPayTestTags.PIN_SUCCESS_TITLE),
text = stringResourceSafe(R.string.tangempay_card_details_change_pin_success_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
SpacerH12()
Text(
modifier = Modifier.padding(horizontal = 32.dp),
modifier = Modifier
.padding(horizontal = 32.dp)
.testTag(TangemPayTestTags.PIN_SUCCESS_DESCRIPTION),
text = stringResourceSafe(R.string.tangempay_card_details_change_pin_success_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,

View file

@ -17,6 +17,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
@ -31,6 +32,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayChangePinUM
import kotlinx.coroutines.delay
@ -52,7 +54,7 @@ internal fun TangemPayChangePinScreen(
)
Column(
modifier = modifier
modifier = Modifier
.fillMaxWidth()
.padding(top = 48.dp)
.padding(horizontal = 36.dp)
@ -64,6 +66,7 @@ internal fun TangemPayChangePinScreen(
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.testTag(TangemPayTestTags.PIN_SCREEN_TITLE),
)
SpacerH16()
@ -73,6 +76,7 @@ internal fun TangemPayChangePinScreen(
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.testTag(TangemPayTestTags.PIN_SCREEN_DESCRIPTION),
)
SpacerH(26.dp)
@ -84,7 +88,8 @@ internal fun TangemPayChangePinScreen(
modifier = Modifier
.imePadding()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
.fillMaxWidth()
.testTag(TangemPayTestTags.PIN_SUBMIT_BUTTON),
primaryButton = NavigationButton(
textReference = resourceReference(R.string.common_submit),
onClick = state.onSubmitClick,
@ -117,6 +122,7 @@ private fun PinCodeSection(state: TangemPayChangePinUM, modifier: Modifier = Mod
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.warning,
textAlign = TextAlign.Center,
modifier = Modifier.testTag(TangemPayTestTags.PIN_ERROR_MESSAGE),
)
}
}
@ -150,7 +156,8 @@ private fun PinCode(
.clickable {
focusRequester.requestFocus()
keyboardController?.show()
},
}
.testTag(TangemPayTestTags.PIN_INPUT_FIELD),
textStyle = TangemTheme.typography.h1.copy(color = Transparent),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.NumberPassword,

View file

@ -49,6 +49,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.core.ui.test.TokenDetailsTopBarTestTags
import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags
import com.tangem.features.tangempay.components.express.PreviewEmptyExpressTransactionsComponent
@ -304,7 +305,7 @@ private fun FiatBalance(
),
)
is TangemPayDetailsBalanceBlockState.Content -> Text(
modifier = modifier,
modifier = modifier.testTag(TangemPayTestTags.PAYMENT_ACCOUNT_BALANCE),
text = state.fiatBalance.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.h2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
@ -312,7 +313,7 @@ private fun FiatBalance(
),
)
is TangemPayDetailsBalanceBlockState.Error -> Text(
modifier = modifier,
modifier = modifier.testTag(TangemPayTestTags.PAYMENT_ACCOUNT_BALANCE),
text = DASH_SIGN.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
@ -31,6 +32,7 @@ import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.tangempay.main.impl.R
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -90,7 +92,8 @@ private fun TangemPayMainContent(
modifier = modifier
.clip(RoundedCornerShape(size = 18.dp))
.background(TangemTheme.colors2.surface.level3)
.clickableSingle(onClick = payMainUM.onClick),
.clickableSingle(onClick = payMainUM.onClick)
.testTag(TangemPayTestTags.MAIN_SCREEN_TILE),
) {
Image(
painter = painterResource(R.drawable.img_visa_36),

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -29,6 +30,7 @@ import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.tangempay.main.impl.R
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -57,7 +59,7 @@ private fun TangemPayMainBlockContent(
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier,
modifier = modifier.testTag(TangemPayTestTags.MAIN_SCREEN_TILE),
shape = TangemTheme.shapes.roundedCornersXMedium,
color = TangemTheme.colors.background.primary,
onClick = state.onClick,