Updated on 2026-08-14
This commit is contained in:
parent
616386c345
commit
b81ea5cf6e
9 changed files with 133 additions and 59 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,4 +39,7 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue