Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-22 20:07:04 +05:00
parent 616386c345
commit b81ea5cf6e
9 changed files with 133 additions and 59 deletions

View file

@ -9,6 +9,7 @@ import com.tangem.domain.pay.DataForReceiveFactory
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
@ -52,6 +53,14 @@ internal interface TangemPayDataModule {
return TangemPayMainScreenCustomerInfoUseCase(repository = repository)
}
@Provides
@Singleton
fun provideProduceTangemPayInitialDataUseCase(
repository: OnboardingRepository,
): ProduceTangemPayInitialDataUseCase {
return ProduceTangemPayInitialDataUseCase(repository = repository)
}
@Provides
@Singleton
fun provideTangemPayIssueOrderUseCase(repository: OnboardingRepository): TangemPayIssueOrderUseCase {

View file

@ -2,12 +2,14 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.data.pay.util.TangemPayWalletsManager
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
import com.tangem.datasource.api.pay.models.request.OrderRequest
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
import com.tangem.datasource.local.visa.TangemPayStorage
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
@ -27,6 +29,8 @@ internal class DefaultOnboardingRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val tangemPayStorage: TangemPayStorage,
private val authDataSource: TangemPayAuthDataSource,
private val tangemPayWalletsManager: TangemPayWalletsManager,
) : OnboardingRepository {
override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> {
@ -38,6 +42,31 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
}
override suspend fun isTangemPayInitialDataProduced(): Boolean {
val walletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId
val customerWalletAddress = tangemPayStorage.getCustomerWalletAddress(walletId) ?: return false
tangemPayStorage.getAuthTokens(customerWalletAddress) ?: return false
return true
}
override suspend fun produceInitialData() {
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay()
val initialCredentials = authDataSource.produceInitialCredentials(cardId = wallet.cardId)
.fold(
ifLeft = { error -> error("Can not produce initial data: ${error.message}") },
ifRight = { it },
)
tangemPayStorage.storeCustomerWalletAddress(
userWalletId = wallet.walletId,
customerWalletAddress = initialCredentials.customerWalletAddress,
)
tangemPayStorage.storeAuthTokens(
customerWalletAddress = initialCredentials.customerWalletAddress,
tokens = initialCredentials.authTokens,
)
}
override suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo> {
return requestHelper.runWithErrorLogs(TAG) {
val result = requestHelper.request { authHeader ->
@ -85,7 +114,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
override suspend fun createOrder(): Either<UniversalError, Unit> = withContext(dispatcherProvider.io) {
requestHelper.runWithErrorLogs(TAG) {
val walletAddress = requestHelper.getCustomerWalletAddress()
val result = requestHelper.requestWithPersistedToken { authHeader ->
val result = requestHelper.request { authHeader ->
tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress))
}.result ?: error("Create order result is null")
@ -129,7 +158,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
private suspend fun getCustomerInfoWithPersistedToken(): CustomerInfo {
val result = requestHelper.requestWithPersistedToken { authHeader ->
val result = requestHelper.request { authHeader ->
tangemPayApi.getCustomerMe(authHeader)
}.result
return getCustomerInfo(result)

View file

@ -63,15 +63,6 @@ internal class TangemPayRequestPerformer @Inject constructor(
)
}
suspend fun <T : Any> requestWithPersistedToken(requestBlock: suspend (header: String) -> ApiResponse<T>): T =
withContext(dispatchers.io) {
performRequest(
requestBlock = requestBlock,
getTokens = { getAccessTokensIfSaved() ?: error("Cannot get saved access tokens") },
refreshTokens = ::refreshAuthTokens,
)
}
private suspend fun <T : Any> performRequest(
requestBlock: suspend (header: String) -> ApiResponse<T>,
getTokens: (suspend () -> VisaAuthTokens),
@ -128,40 +119,20 @@ internal class TangemPayRequestPerformer @Inject constructor(
}
private suspend fun getAccessTokens(): VisaAuthTokens {
return getAccessTokensIfSaved() ?: fetchTokens()
}
private suspend fun getAccessTokensIfSaved(): VisaAuthTokens? {
return tangemPayStorage.getAuthTokens(getCustomerWalletAddress())
}
private suspend fun fetchTokens(): VisaAuthTokens {
val wallet = tangemPayWalletsManager.getDefaultWalletForTangemPay()
val initialCredentials = authDataSource.produceInitialCredentials(cardId = wallet.cardId)
.getOrThrowWithMessage("Can not produce initial data:")
tangemPayStorage.storeCustomerWalletAddress(
userWalletId = wallet.walletId,
customerWalletAddress = initialCredentials.customerWalletAddress,
)
tangemPayStorage.storeAuthTokens(
customerWalletAddress = initialCredentials.customerWalletAddress,
tokens = initialCredentials.authTokens,
)
return initialCredentials.authTokens
val walletAddress = getCustomerWalletAddress()
val tokens = tangemPayStorage.getAuthTokens(walletAddress) ?: error("Auth tokens are not stored")
return tokens
}
private suspend fun refreshAuthTokens(): VisaAuthTokens {
val customerWalletAddress = getCustomerWalletAddress()
val refreshToken = getAccessTokens().refreshToken.value
val tokens = authDataSource.refreshAuthTokens(refreshToken).getOrThrowWithMessage("Cannot refresh tokens:")
val tokens = authDataSource.refreshAuthTokens(refreshToken)
.fold(
ifLeft = { error -> error("Cannot refresh tokens: ${error.message}") },
ifRight = { it },
)
tangemPayStorage.storeAuthTokens(customerWalletAddress, tokens)
return tokens
}
private fun <A : Throwable, B> Either<A, B>.getOrThrowWithMessage(message: String): B {
return this.fold(
ifLeft = { error -> throw IllegalStateException("$message ${error.message}") },
ifRight = { it },
)
}
}

View file

@ -9,6 +9,10 @@ interface OnboardingRepository {
suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean>
suspend fun isTangemPayInitialDataProduced(): Boolean
suspend fun produceInitialData()
suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo>
suspend fun createOrder(): Either<UniversalError, Unit>

View file

@ -0,0 +1,21 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.pay.repository.OnboardingRepository
class ProduceTangemPayInitialDataUseCase(
private val repository: OnboardingRepository,
) {
suspend operator fun invoke(): Either<Throwable, Unit> {
return either {
val isDataProduced = repository.isTangemPayInitialDataProduced()
if (isDataProduced) {
return@either Unit
} else {
repository.produceInitialData()
}
}
}
}

View file

@ -39,4 +39,7 @@ dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Other */
implementation(deps.timber)
}

View file

@ -21,7 +21,7 @@ internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.screenState.collectAsStateWithLifecycle()
val state by model.uiState.collectAsStateWithLifecycle()
TandemPayOnboardingScreen(modifier = modifier, state = state)
}

View file

@ -7,6 +7,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Stable
@ -23,11 +25,11 @@ internal class TangemPayOnboardingModel @Inject constructor(
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val repository: OnboardingRepository,
private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
) : Model() {
private val params = paramsContainer.require<TangemPayOnboardingComponent.Params>()
val screenState: StateFlow<TangemPayOnboardingScreenState>
val uiState: StateFlow<TangemPayOnboardingScreenState>
field = MutableStateFlow(getInitialState())
init {
@ -45,18 +47,8 @@ internal class TangemPayOnboardingModel @Inject constructor(
}
}
private fun openKyc() {
router.replaceAll(AppRoute.Wallet, AppRoute.Kyc)
}
private fun back() {
router.pop()
}
private fun showOnboarding() {
screenState.update {
it.copy(fullScreenLoading = false)
}
uiState.update { it.copy(fullScreenLoading = false) }
}
private suspend fun checkCustomerInfo() {
@ -66,7 +58,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
!customerInfo.isKycApproved -> {
when (params) {
is TangemPayOnboardingComponent.Params.Deeplink ->
screenState.value = screenState.value.copy(fullScreenLoading = false)
uiState.value = uiState.value.copy(fullScreenLoading = false)
else -> openKyc()
}
}
@ -76,11 +68,46 @@ internal class TangemPayOnboardingModel @Inject constructor(
.onLeft { back() }
}
private fun onGetCardClick() {
uiState.update { it.copy(buttonLoading = true) }
modelScope.launch {
val result = produceInitialDataUseCase()
if (result.isLeft()) {
Timber.e("Error producing initial data: ${result.leftOrNull()?.message}")
uiState.update { it.copy(buttonLoading = false) }
return@launch
}
repository.getCustomerInfo()
.fold(
ifLeft = {
Timber.e("Error getCustomerInfo: ${it.errorCode}")
uiState.update { it.copy(buttonLoading = false) }
},
ifRight = { customerInfo ->
if (customerInfo.isKycApproved) {
back()
} else {
openKyc()
}
},
)
}
}
private fun openKyc() {
router.replaceAll(AppRoute.Wallet, AppRoute.Kyc)
}
private fun back() {
router.pop()
}
private fun getInitialState(): TangemPayOnboardingScreenState {
return TangemPayOnboardingScreenState(
fullScreenLoading = true,
buttonLoading = false,
onGetCardClick = ::openKyc,
onGetCardClick = ::onGetCardClick,
onBackClick = ::back,
)
}

View file

@ -18,6 +18,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.settings.*
@ -93,6 +94,7 @@ internal class WalletModel @Inject constructor(
private val tangemPayIssueOrderUseCase: TangemPayIssueOrderUseCase,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase,
private val tangemPayOnboardingRepository: OnboardingRepository,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
@ -344,16 +346,24 @@ internal class WalletModel @Inject constructor(
* and every minute while user stays on the main screen
*/
screenLifecycleProvider.isBackgroundState.onEach { inBackground ->
// fast exit
if (!tangemPayFeatureToggles.isTangemPayEnabled) return@onEach
updateTangemPayJobHolder.cancel()
if (!inBackground && tangemPayFeatureToggles.isTangemPayEnabled) {
modelScope.launch {
modelScope.launch {
// fast exit
val initialDataProduced = tangemPayOnboardingRepository.isTangemPayInitialDataProduced()
if (!initialDataProduced) return@launch
if (!inBackground) {
refreshTangemPayInfo()
while (isActive) {
delay(TANGEM_PAY_UPDATE_INTERVAL)
refreshTangemPayInfo()
}
}.saveIn(updateTangemPayJobHolder)
}
}
}.saveIn(updateTangemPayJobHolder)
}.launchIn(modelScope)
}