From 210ba5ca5963faed356790a92d92c8bb5a6839ff Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Sep 2025 12:34:23 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 23 +++++- .../tangem/datasource/api/pay/TangemPayApi.kt | 13 ++++ .../api/pay/models/request/OrderRequest.kt | 9 +++ .../api/pay/models/response/OrderResponse.kt | 33 ++++++++ .../datasource/local/visa/TangemPayStorage.kt | 4 + .../components/notifications/Notification.kt | 2 + .../notifications/NotificationConfig.kt | 1 + .../tangem/data/pay/di/TangemPayDataModule.kt | 7 ++ .../repository/DefaultOnboardingRepository.kt | 75 +++++++++++++++---- .../DefaultTangemPayTxHistoryRepository.kt | 15 ++-- .../tangem/domain/pay/model/CustomerInfo.kt | 8 +- .../tangem/domain/pay/model/OrderStatus.kt | 9 +++ .../pay/repository/OnboardingRepository.kt | 5 +- .../pay/usecase/TangemPayIssueOrderUseCase.kt | 10 +++ .../TangemPayMainScreenCustomerInfoUseCase.kt | 4 +- .../model/TangemPayOnboardingModel.kt | 3 - .../wallet/child/wallet/model/WalletModel.kt | 50 ++++++++++--- .../router/DefaultWalletRouter.kt | 4 + .../presentation/router/InnerWalletRouter.kt | 2 + .../wallet/state/model/TangemPayState.kt | 1 + .../converter/TangemPayStateConverter.kt | 62 +++++++++------ .../visa/TangemPayMainScreenBlock.kt | 53 +++++++------ 22 files changed, 303 insertions(+), 90 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/OrderRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayIssueOrderUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 19171195db..58e979c7dc 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -14,6 +14,7 @@ import javax.inject.Singleton import kotlin.text.encodeToByteArray private const val DEFAULT_KEY = "tangem_pay_default_key" +private const val ORDER_ID_KEY = "tangem_pay_order_id_key" @Singleton internal class DefaultTangemPayStorage @Inject constructor( @@ -53,9 +54,25 @@ internal class DefaultTangemPayStorage @Inject constructor( ?.let(tokensAdapter::fromJson) } - override suspend fun clear(customerWalletAddress: String) = withContext(dispatcherProvider.io) { - secureStorage.delete(createKey(customerWalletAddress)) + override suspend fun storeOrderId(customerWalletAddress: String, orderId: String) { + withContext(dispatcherProvider.io) { + secureStorage.store( + orderId.encodeToByteArray(throwOnInvalidSequence = true), + createOrderIdKey(customerWalletAddress), + ) + } } - private fun createKey(cardId: String): String = "${DEFAULT_KEY}_$cardId" + override suspend fun getOrderId(customerWalletAddress: String): String? = withContext(dispatcherProvider.io) { + secureStorage.get(createOrderIdKey(customerWalletAddress))?.decodeToString(throwOnInvalidSequence = true) + } + + override suspend fun clear(customerWalletAddress: String) = withContext(dispatcherProvider.io) { + secureStorage.delete(createKey(customerWalletAddress)) + secureStorage.delete(createOrderIdKey(customerWalletAddress)) + } + + private fun createKey(address: String): String = "${DEFAULT_KEY}_$address" + + private fun createOrderIdKey(address: String): String = "${ORDER_ID_KEY}_$address" } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 87a22537d3..9e3d771b52 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -7,6 +7,7 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST +import retrofit2.http.Path import retrofit2.http.Query private const val TX_HISTORY_PAGING_DEFAULT_LIMIT = 20 @@ -124,4 +125,16 @@ interface TangemPayApi { @POST("v1/deeplink/validate") suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse + + @GET("v1/order/{order_id}") + suspend fun getOrder( + @Header("Authorization") authHeader: String, + @Path("order_id") orderId: String, + ): ApiResponse + + @POST("v1/order") + suspend fun createOrder( + @Header("Authorization") authHeader: String, + @Body body: OrderRequest, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/OrderRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/OrderRequest.kt new file mode 100644 index 0000000000..73725d5782 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/OrderRequest.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class OrderRequest( + @Json(name = "wallet_address") val walletAddress: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt new file mode 100644 index 0000000000..e75134b059 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt @@ -0,0 +1,33 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class OrderResponse( + @Json(name = "result") val result: Result?, + @Json(name = "error") val error: String?, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "id") val id: String, + @Json(name = "customer_id") val customerId: String?, + @Json(name = "type") val type: String?, + @Json(name = "status") val status: String, + @Json(name = "step") val step: String?, + @Json(name = "data") val data: Data, + @Json(name = "step_change_code") val stepChangeCode: Int?, + @Json(name = "created_at") val createdAt: String?, + @Json(name = "updated_at") val updatedAt: String?, + ) { + @JsonClass(generateAdapter = true) + data class Data( + @Json(name = "type") val type: String?, + @Json(name = "specification_name") val specificationName: String?, + @Json(name = "customer_wallet_address") val customerWalletAddress: String, + @Json(name = "emboss_name") val embossName: String?, + @Json(name = "product_instance_id") val productInstanceId: String?, + @Json(name = "payment_account_id") val paymentAccountId: String?, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index edf36a599f..1ed58f2220 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -8,5 +8,9 @@ interface TangemPayStorage { suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens? + suspend fun storeOrderId(customerWalletAddress: String, orderId: String) + + suspend fun getOrderId(customerWalletAddress: String): String? + suspend fun clear(customerWalletAddress: String) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 50f8841ef5..94026d5ae3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -256,6 +256,7 @@ private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButt modifier = Modifier.fillMaxWidth(), iconResId = config.iconResId, enabled = isEnabled, + showProgress = config.showProgress, ) } else { SecondaryButton( @@ -264,6 +265,7 @@ private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButt modifier = Modifier.fillMaxWidth(), size = TangemButtonSize.WideAction, enabled = isEnabled, + showProgress = config.showProgress, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt index bd4d9dacef..fab4badd0c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -44,6 +44,7 @@ data class NotificationConfig( val text: TextReference, @DrawableRes val iconResId: Int? = null, val onClick: () -> Unit, + val showProgress: Boolean = false, ) : ButtonsState() data class PairButtonsConfig( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index a29a800638..30b4c347fa 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -5,6 +5,7 @@ import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository import com.tangem.data.pay.repository.DefaultOnboardingRepository import com.tangem.domain.pay.repository.KycRepository 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.tangempay.repository.TangemPayTxHistoryRepository import dagger.Binds @@ -38,5 +39,11 @@ internal interface TangemPayDataModule { ): TangemPayMainScreenCustomerInfoUseCase { return TangemPayMainScreenCustomerInfoUseCase(repository = repository) } + + @Provides + @Singleton + fun provideTangemPayIssueOrderUseCase(repository: OnboardingRepository): TangemPayIssueOrderUseCase { + return TangemPayIssueOrderUseCase(repository = repository) + } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 0307d16174..e626dc3cc5 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -1,40 +1,68 @@ package com.tangem.data.pay.repository import arrow.core.Either -import arrow.core.raise.either import com.tangem.core.error.UniversalError 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.domain.pay.model.CustomerInfo.CardInfo +import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.domain.pay.model.CustomerInfo.CardInfo import com.tangem.domain.pay.model.CustomerInfo.ProductInstance +import com.tangem.domain.pay.model.MainScreenCustomerInfo +import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.OnboardingRepository import javax.inject.Inject private const val VALID_STATUS = "valid" +private const val TAG = "TangemPay: OnboardingRepository" internal class DefaultOnboardingRepository @Inject constructor( private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, + private val tangemPayStorage: TangemPayStorage, ) : OnboardingRepository { - override suspend fun validateDeeplink(link: String): Either = either { - return requestHelper.request { - tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link)) - }.map { it.result?.status == VALID_STATUS } + override suspend fun validateDeeplink(link: String): Either { + return requestHelper.runWithErrorLogs(TAG) { + val result = requestHelper.request { + tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link)) + }.result + result?.status == VALID_STATUS + } } - override suspend fun getCustomerInfo(): Either = either { - return requestHelper.request { authHeader -> - tangemPayApi.getCustomerMe(authHeader) - }.map { getCustomerInfo(it.result) } + override suspend fun getCustomerInfo(): Either { + return requestHelper.runWithErrorLogs(TAG) { + val result = requestHelper.request { authHeader -> + tangemPayApi.getCustomerMe(authHeader) + }.result + getCustomerInfo(result) + } } - override suspend fun getMainScreenCustomerInfo(): Either = either { - return requestHelper.requestWithPersistedToken { authHeader -> - tangemPayApi.getCustomerMe(authHeader) - }.map { getCustomerInfo(it.result) } + override suspend fun getMainScreenCustomerInfo(): Either { + return requestHelper.runWithErrorLogs(TAG) { + val result = requestHelper.requestWithPersistedToken { authHeader -> + tangemPayApi.getCustomerMe(authHeader) + }.result + + val orderStatus = getOrderStatus().getOrNull() ?: error("Order status is null") + + MainScreenCustomerInfo(info = getCustomerInfo(result), orderStatus = orderStatus) + } + } + + override suspend fun createOrder(): Either { + return requestHelper.runWithErrorLogs(TAG) { + val walletAddress = requestHelper.getCustomerWalletAddress() + val result = requestHelper.requestWithPersistedToken { authHeader -> + tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress)) + }.result ?: error("Create order result is null") + + tangemPayStorage.storeOrderId(result.data.customerWalletAddress, result.id) + } } private fun getCustomerInfo(response: CustomerMeResponse.Result?): CustomerInfo { @@ -55,4 +83,23 @@ internal class DefaultOnboardingRepository @Inject constructor( cardInfo = cardInfo, ) } + + private suspend fun getOrderStatus(): Either { + return requestHelper.runWithErrorLogs(TAG) { + val walletAddress = requestHelper.getCustomerWalletAddress() + val orderId: String = tangemPayStorage.getOrderId(walletAddress) + ?: return@runWithErrorLogs OrderStatus.NOT_ISSUED + + val result = requestHelper.request { authHeader -> + tangemPayApi.getOrder(authHeader, orderId) + }.result ?: error("Order result is null") + + when (result.status) { + OrderStatus.NEW.apiName -> OrderStatus.NEW + OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING + OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED + else -> OrderStatus.CANCELED + } + } + } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt index f104ae6725..a5013ee703 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt @@ -19,6 +19,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import javax.inject.Inject private const val INITIAL_CURSOR = "initial_cursor_key" +private const val TAG = "TangemPay: TangemPayTxHistoryRepository:" internal class DefaultTangemPayTxHistoryRepository @Inject constructor( private val requestPerformer: TangemPayRequestPerformer, @@ -80,15 +81,11 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( } private suspend fun fetch(userWalletId: UserWalletId, cursor: String?, pageSize: Int) { - val response = requestPerformer.request { authHeader -> - visaApi.getTangemPayTxHistory( - authHeader = authHeader, - limit = pageSize, - cursor = cursor, - ) - }.getOrNull() - response?.let { - val items = TangemPayTxHistoryItemConverter.convertList(response.result.transactions) + requestPerformer.runWithErrorLogs(TAG) { + val result = requestPerformer.request { authHeader -> + visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor) + }.result + val items = TangemPayTxHistoryItemConverter.convertList(result.transactions) txHistoryItemsStore.store(key = userWalletId, cursor = cursor ?: INITIAL_CURSOR, value = items) } } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index bee0c16152..3341b39605 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -3,7 +3,11 @@ package com.tangem.domain.pay.model import java.math.BigDecimal private const val APPROVED_KYC_STATUS = "APPROVED" -private const val ACTIVE_PI_STATUS = "active" + +data class MainScreenCustomerInfo( + val info: CustomerInfo, + val orderStatus: OrderStatus, +) data class CustomerInfo( val productInstance: ProductInstance?, @@ -23,6 +27,4 @@ data class CustomerInfo( ) fun isKycApproved() = kycStatus == APPROVED_KYC_STATUS - - fun isProductInstanceActive() = productInstance?.status == ACTIVE_PI_STATUS } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt new file mode 100644 index 0000000000..6cab68a6fd --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.pay.model + +enum class OrderStatus(val apiName: String) { + NOT_ISSUED(""), + NEW("NEW"), + PROCESSING("PROCESSING"), + COMPLETED("COMPLETED"), + CANCELED("CANCELED"), +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index d6a888bc10..5c93a97e19 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -3,6 +3,7 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.domain.pay.model.MainScreenCustomerInfo interface OnboardingRepository { @@ -10,8 +11,10 @@ interface OnboardingRepository { suspend fun getCustomerInfo(): Either + suspend fun createOrder(): Either + /** * Returns only if the user already authorised at least once */ - suspend fun getMainScreenCustomerInfo(): Either + suspend fun getMainScreenCustomerInfo(): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayIssueOrderUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayIssueOrderUseCase.kt new file mode 100644 index 0000000000..2cf992c3d8 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayIssueOrderUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.pay.usecase + +import com.tangem.domain.pay.repository.OnboardingRepository + +class TangemPayIssueOrderUseCase( + private val repository: OnboardingRepository, +) { + + suspend operator fun invoke(): Unit? = repository.createOrder().getOrNull() +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index cefdd9f95a..1f49a481e2 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -1,6 +1,6 @@ package com.tangem.domain.pay.usecase -import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.repository.OnboardingRepository /** @@ -11,5 +11,5 @@ class TangemPayMainScreenCustomerInfoUseCase( private val repository: OnboardingRepository, ) { - suspend operator fun invoke(): CustomerInfo? = repository.getMainScreenCustomerInfo().getOrNull() + suspend operator fun invoke(): MainScreenCustomerInfo? = repository.getMainScreenCustomerInfo().getOrNull() } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index bb4f1621ce..65caca46f0 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -63,9 +63,6 @@ internal class TangemPayOnboardingModel @Inject constructor( else -> openKyc() } } - !customerInfo.isProductInstanceActive() -> { - // TODO [REDACTED_TASK_KEY]: create order and poll order status (API is not ready yet) - } else -> back() } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index db55f79904..fd4e553563 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -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.usecase.TangemPayIssueOrderUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase @@ -51,6 +52,8 @@ import kotlinx.coroutines.flow.* import timber.log.Timber import javax.inject.Inject +private const val TANGEM_PAY_UPDATE_INTERVAL = 60000L + @Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped @@ -88,6 +91,7 @@ internal class WalletModel @Inject constructor( private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val userWalletsListRepository: UserWalletsListRepository, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, + private val tangemPayIssueOrderUseCase: TangemPayIssueOrderUseCase, private val tangemPayStateConverter: TangemPayStateConverter, private val tangemPayFeatureToggles: TangemPayFeatureToggles, val screenLifecycleProvider: ScreenLifecycleProvider, @@ -105,6 +109,8 @@ internal class WalletModel @Inject constructor( private var expressTxStatusTaskScheduler = SingleTaskScheduler() + private var updateTangemPayJob: Job? = null + init { analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) @@ -323,20 +329,44 @@ internal class WalletModel @Inject constructor( private fun subscribeToTangemPayInfo() { /** * Update state each time a user opens/returns to wallet screen + * and every minute while user stays on the main screen */ - screenLifecycleProvider.isBackgroundState.onEach { isBackground -> - if (!isBackground) { updateTangemPayInfo() } + screenLifecycleProvider.isBackgroundState.onEach { inBackground -> + if (!inBackground && tangemPayFeatureToggles.isTangemPayEnabled) { + updateTangemPayJob = modelScope.launch { + refreshTangemPayInfo() + while (isActive) { + delay(TANGEM_PAY_UPDATE_INTERVAL) + refreshTangemPayInfo() + } + } + } else { + updateTangemPayJob?.cancel() + updateTangemPayJob = null + } }.launchIn(modelScope) } - private fun updateTangemPayInfo() { - if (tangemPayFeatureToggles.isTangemPayEnabled) { - modelScope.launch { - tangemPayMainScreenCustomerInfoUseCase()?.let { info -> - stateHolder.update { - it.copy(tangemPayState = tangemPayStateConverter.convert(info)) - } - } + private suspend fun refreshTangemPayInfo() { + val newState = withContext(dispatchers.io) { + tangemPayMainScreenCustomerInfoUseCase()?.let { info -> + tangemPayStateConverter.convert( + value = info, + onIssueOrderClick = ::issueOrder, + onContinueKycClick = innerWalletRouter::openTangemPayOnboarding, + ) + } + } ?: return + stateHolder.update { it.copy(tangemPayState = newState) } + } + + private fun issueOrder() { + modelScope.launch { + stateHolder.update { it.copy(tangemPayState = tangemPayStateConverter.getIssueProgress()) } + withContext(dispatchers.io) { + tangemPayIssueOrderUseCase() + } ?: stateHolder.update { + it.copy(tangemPayState = tangemPayStateConverter.getIssueState(::issueOrder)) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 2ac5b6e73f..f86ace0f5c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -107,4 +107,8 @@ internal class DefaultWalletRouter @Inject constructor( configuration = WalletDialogConfig.TokenReceive(tokenReceiveConfig), ) } + + override fun openTangemPayOnboarding() { + router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding)) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index c04b7026ce..110b72c156 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -58,4 +58,6 @@ internal interface InnerWalletRouter { fun openNFT(userWallet: UserWallet) fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) + + fun openTangemPayOnboarding() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt index cced51be15..4e08ae642c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TangemPayState.kt @@ -14,6 +14,7 @@ internal sealed class TangemPayState { val buttonText: TextReference, @DrawableRes val iconRes: Int, val onButtonClick: () -> Unit, + val showProgress: Boolean = false, ) : TangemPayState() data class Card( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt index b2afc216b5..5fb3f32ccd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt @@ -1,48 +1,64 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter -import com.tangem.common.routing.AppRoute import com.tangem.common.ui.R -import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.pay.model.CustomerInfo.CardInfo -import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.domain.pay.model.MainScreenCustomerInfo +import com.tangem.domain.pay.model.OrderStatus import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState -import com.tangem.utils.converter.Converter +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress import java.util.Currency import javax.inject.Inject -internal class TangemPayStateConverter @Inject constructor( - private val router: Router, -) : Converter { +internal class TangemPayStateConverter @Inject constructor() { - override fun convert(value: CustomerInfo): TangemPayState { - val route = AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding) - val cardInfo = value.cardInfo + fun convert( + value: MainScreenCustomerInfo, + onIssueOrderClick: () -> Unit, + onContinueKycClick: () -> Unit, + ): TangemPayState { + val cardInfo = value.info.cardInfo return when { - !value.isKycApproved() -> TangemPayState.Progress( - title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), - buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), - iconRes = R.drawable.ic_promo_kyc_36, - onButtonClick = { router.push(route) }, - ) - !value.isProductInstanceActive() -> TangemPayState.Progress( - title = TextReference.Res(R.string.tangempay_issue_card_notification_title), - buttonText = TextReference.Res(R.string.common_continue), - iconRes = R.drawable.ic_tangem_pay_promo_card_36, - onButtonClick = { router.push(route) }, - ) + !value.info.isKycApproved() -> { + Progress( + title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), + buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), + iconRes = R.drawable.ic_promo_kyc_36, + onButtonClick = onContinueKycClick, + ) + } + value.orderStatus == OrderStatus.NOT_ISSUED || value.orderStatus == OrderStatus.CANCELED -> { + getIssueState(onIssueOrderClick) + } cardInfo != null -> { TangemPayState.Card( lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"), balanceText = TextReference.Str(getBalanceText(cardInfo)), ) } - else -> TangemPayState.Empty + else -> { + getIssueProgress() + } } } + fun getIssueProgress(): TangemPayState = Progress( + title = TextReference.Res(R.string.tangempay_issue_card_notification_title), + buttonText = TextReference.EMPTY, + iconRes = R.drawable.ic_tangem_pay_promo_card_36, + onButtonClick = {}, + showProgress = true, + ) + + fun getIssueState(onIssueOrderClick: () -> Unit) = Progress( + title = TextReference.Res(R.string.tangempay_issue_card_notification_title), + buttonText = TextReference.Res(R.string.common_continue), + iconRes = R.drawable.ic_tangem_pay_promo_card_36, + onButtonClick = onIssueOrderClick, + ) + private fun getBalanceText(cardInfo: CardInfo): String { val currency = Currency.getInstance(cardInfo.currencyCode) return cardInfo.balance.format { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt index 2f03ffe4e0..786e56b157 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.visa import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -16,31 +15,31 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock @Composable internal fun TangemPayMainScreenBlock(state: TangemPayState, modifier: Modifier = Modifier) { - AnimatedVisibility(state !is TangemPayState.Empty) { - when (state) { - is TangemPayState.Progress -> { - Notification( - modifier = modifier - .fillMaxWidth(), - config = NotificationConfig( - title = state.title, - iconSize = 36.dp, - subtitle = TextReference.EMPTY, - iconResId = state.iconRes, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = state.buttonText, - onClick = state.onButtonClick, - ), + when (state) { + is Progress -> { + Notification( + modifier = modifier + .fillMaxWidth(), + config = NotificationConfig( + title = state.title, + iconSize = 36.dp, + subtitle = TextReference.EMPTY, + iconResId = state.iconRes, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = state.buttonText, + onClick = state.onButtonClick, + showProgress = state.showProgress, ), - ) - } - is TangemPayState.Card -> TangemPayCardMainBlock(state, modifier) - is TangemPayState.Empty -> Unit + ), + ) } + is TangemPayState.Card -> TangemPayCardMainBlock(state, modifier) + is TangemPayState.Empty -> Unit } } @@ -51,7 +50,7 @@ private fun ResetCardScreenPreview() { TangemThemePreview { Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { TangemPayMainScreenBlock( - TangemPayState.Progress( + Progress( title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), iconRes = R.drawable.ic_promo_kyc_36, @@ -60,7 +59,7 @@ private fun ResetCardScreenPreview() { ) TangemPayMainScreenBlock( - TangemPayState.Progress( + Progress( title = TextReference.Res(R.string.tangempay_issue_card_notification_title), buttonText = TextReference.Res(R.string.common_continue), iconRes = R.drawable.ic_tangem_pay_promo_card_36, @@ -68,6 +67,16 @@ private fun ResetCardScreenPreview() { ), ) + TangemPayMainScreenBlock( + Progress( + title = TextReference.Res(R.string.tangempay_issue_card_notification_title), + buttonText = TextReference.EMPTY, + iconRes = R.drawable.ic_tangem_pay_promo_card_36, + onButtonClick = {}, + showProgress = true, + ), + ) + TangemPayMainScreenBlock( TangemPayState.Card( lastFourDigits = TextReference.Str("*1234"),