Updated on 2026-08-14
This commit is contained in:
parent
ff55704d32
commit
9e2eb5710b
82 changed files with 1150 additions and 401 deletions
|
|
@ -9,7 +9,7 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
|||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import com.tangem.sdk.storage.AndroidSecureStorageV2
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
|
|
@ -36,7 +36,7 @@ internal class DefaultTangemPayStorage @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) }
|
||||
private val tokensAdapter by lazy { moshi.adapter(TangemPayAuthTokens::class.java) }
|
||||
|
||||
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
|
|
@ -50,7 +50,7 @@ internal class DefaultTangemPayStorage @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) =
|
||||
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens) =
|
||||
withContext(dispatcherProvider.io) {
|
||||
val json = tokensAdapter.toJson(tokens)
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ internal class DefaultTangemPayStorage @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens? =
|
||||
override suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens? =
|
||||
withContext(dispatcherProvider.io) {
|
||||
secureStorage.get(createKey(customerWalletAddress))
|
||||
?.decodeToString(throwOnInvalidSequence = true)
|
||||
|
|
@ -98,6 +98,14 @@ internal class DefaultTangemPayStorage @Inject constructor(
|
|||
secureStorage.delete(createOrderIdKey(customerWalletAddress))
|
||||
}
|
||||
|
||||
override suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.store(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId), true)
|
||||
}
|
||||
|
||||
override suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? {
|
||||
return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId))
|
||||
}
|
||||
|
||||
override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) =
|
||||
withContext(dispatcherProvider.io) {
|
||||
secureStorage.delete(createCustomerAddressKey(userWalletId))
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import com.tangem.domain.visa.model.TangemPayInitialCredentials
|
|||
import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet
|
||||
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
|
||||
import com.tangem.domain.visa.model.sign
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -44,14 +45,17 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor(
|
|||
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
|
||||
?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)
|
||||
|
||||
val derivationResult = runDerivationTask(session, wallet)
|
||||
val address = when (derivationResult) {
|
||||
val address = when (val derivationResult = runDerivationTask(session, wallet)) {
|
||||
is CompletionResult.Failure<*> -> return CompletionResult.Failure(derivationResult.error)
|
||||
is CompletionResult.Success<ExtendedPublicKey> -> generateAddressFromExtendedKey(derivationResult.data)
|
||||
}
|
||||
|
||||
val userWalletId = UserWalletIdBuilder.walletPublicKey(wallet.publicKey)
|
||||
val challenge = withContext(dispatchersProvider.io) {
|
||||
visaAuthRemoteDataSource.getCustomerWalletAuthChallenge(address)
|
||||
visaAuthRemoteDataSource.getCustomerWalletAuthChallenge(
|
||||
customerWalletAddress = address,
|
||||
customerWalletId = userWalletId.stringValue,
|
||||
)
|
||||
}.getOrElse { return CompletionResult.Failure(it.tangemError) }
|
||||
|
||||
val dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge)
|
||||
|
|
|
|||
|
|
@ -668,7 +668,7 @@ internal class ChildFactory @Inject constructor(
|
|||
is AppRoute.Kyc -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = KycComponent.Params,
|
||||
params = KycComponent.Params(route.userWalletId),
|
||||
componentFactory = kycComponentFactory,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -438,7 +438,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
}
|
||||
|
||||
@Serializable
|
||||
data object Kyc : AppRoute(path = "/kyc")
|
||||
data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc")
|
||||
|
||||
@Serializable
|
||||
data class YieldSupplyPromo(
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
},
|
||||
{
|
||||
"name": "TANGEM_PAY_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.31.0"
|
||||
},
|
||||
{
|
||||
"name": "NEW_TOKEN_RECEIVE_ENABLED",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ sealed class ApiConfig {
|
|||
StakeKit,
|
||||
P2PEthPool,
|
||||
TangemPay,
|
||||
TangemPayAuth,
|
||||
BlockAid,
|
||||
YieldSupply,
|
||||
MoonPay,
|
||||
|
|
@ -39,6 +40,7 @@ sealed class ApiConfig {
|
|||
is StakeKit -> ID.StakeKit
|
||||
is P2PEthPool -> ID.P2PEthPool
|
||||
is TangemPay -> ID.TangemPay
|
||||
is TangemPayAuth -> ID.TangemPayAuth
|
||||
is BlockAid -> ID.BlockAid
|
||||
is YieldSupply -> ID.YieldSupply
|
||||
is MoonPay -> ID.MoonPay
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
internal class TangemPayAuth(
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
) : ApiConfig() {
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs = listOf(
|
||||
createDevEnvironment(),
|
||||
createMockedEnvironment(),
|
||||
createProdEnvironment(),
|
||||
)
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "https://api.dev.us.paera.com/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.us.paera.com/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createHeaders() = mapOf(
|
||||
"version" to ProviderSuspend { appVersionProvider.versionName },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
)
|
||||
}
|
||||
|
|
@ -26,11 +26,6 @@ interface TangemPayApi {
|
|||
@Body request: GenerateNoneByCardWalletRequest,
|
||||
): ApiResponse<GenerateNonceResponse>
|
||||
|
||||
@POST("v1/auth/challenge")
|
||||
suspend fun generateNonceByCustomerWallet(
|
||||
@Body request: GenerateNonceByCustomerWalletRequest,
|
||||
): ApiResponse<GenerateNonceResponse>
|
||||
|
||||
@POST("v1/auth/token")
|
||||
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
|
||||
|
||||
|
|
@ -124,6 +119,12 @@ interface TangemPayApi {
|
|||
@GET("v1/customer/me")
|
||||
suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse>
|
||||
|
||||
@GET("v1/customer/wallets/{customer_wallet_id}")
|
||||
suspend fun checkCustomerWalletId(
|
||||
@Header("X-API-KEY") authHeader: String,
|
||||
@Path("customer_wallet_id") customerWalletId: String,
|
||||
): ApiResponse<CheckCustomerWalletResponse>
|
||||
|
||||
@POST("v1/deeplink/validate")
|
||||
suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse<DeeplinkValidityResponse>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.datasource.api.pay
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.pay.models.request.GenerateNonceByCustomerWalletRequest
|
||||
import com.tangem.datasource.api.pay.models.request.GetTokenByCustomerWalletRequest
|
||||
import com.tangem.datasource.api.pay.models.request.RefreshCustomerWalletAccessTokenRequest
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayGenerateNonceResponse
|
||||
import com.tangem.datasource.api.pay.models.response.TangemPayGetTokensResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface TangemPayAuthApi {
|
||||
|
||||
@POST("auth/challenge")
|
||||
suspend fun generateNonceByCustomerWallet(
|
||||
@Body request: GenerateNonceByCustomerWalletRequest,
|
||||
): ApiResponse<TangemPayGenerateNonceResponse>
|
||||
|
||||
@POST("auth/token")
|
||||
suspend fun getTokenByCustomerWallet(
|
||||
@Body request: GetTokenByCustomerWalletRequest,
|
||||
): ApiResponse<TangemPayGetTokensResponse>
|
||||
|
||||
@POST("auth/token/refresh")
|
||||
suspend fun refreshCustomerWalletAccessToken(
|
||||
@Body request: RefreshCustomerWalletAccessTokenRequest,
|
||||
): ApiResponse<TangemPayGetTokensResponse>
|
||||
}
|
||||
|
|
@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass
|
|||
data class GenerateNonceByCustomerWalletRequest(
|
||||
@Json(name = "auth_type") val authType: String = "customer_wallet",
|
||||
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
|
||||
@Json(name = "customer_wallet_id") val customerWalletId: String,
|
||||
)
|
||||
|
|
@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetTokenByCustomerWalletRequest(
|
||||
@Json(name = "auth_type") val authType: String = "customer_wallet",
|
||||
@Json(name = "auth_type") val authType: String,
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
@Json(name = "signature") val signature: String,
|
||||
@Json(name = "message_format") val messageFormat: String,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RefreshCustomerWalletAccessTokenRequest(
|
||||
@Json(name = "auth_type") val authType: String = "customer_wallet",
|
||||
@Json(name = "auth_type") val authType: String,
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CheckCustomerWalletResponse(
|
||||
@Json(name = "id") val id: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TangemPayGenerateNonceResponse(
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TangemPayGetTokensResponse(
|
||||
@Json(name = "access_token") val accessToken: String,
|
||||
@Json(name = "expires_at") val expiresAt: Long,
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
@Json(name = "refresh_expires_at") val refreshExpiresAt: Long,
|
||||
)
|
||||
|
|
@ -80,6 +80,12 @@ internal object ApiConfigsModule {
|
|||
@IntoSet
|
||||
fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPay(appVersionProvider)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemPayAuthConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPayAuth(
|
||||
appVersionProvider,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import com.tangem.datasource.api.news.NewsApi
|
|||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
|
||||
|
|
@ -135,6 +136,15 @@ internal object NetworkModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayAuthApi(retrofitApiBuilder: RetrofitApiBuilder): TangemPayAuthApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.TangemPayAuth,
|
||||
applyTimeoutAnnotations = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBlockAidApi(retrofitApiBuilder: RetrofitApiBuilder): BlockAidApi {
|
||||
|
|
|
|||
|
|
@ -24,4 +24,5 @@ data class EnvironmentConfig(
|
|||
val tangemApiKeyStage: String? = null,
|
||||
val yieldModuleApiKey: String? = null,
|
||||
val yieldModuleApiKeyDev: String? = null,
|
||||
val bffStaticToken: String? = null,
|
||||
)
|
||||
|
|
@ -32,6 +32,7 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
|
|||
tangemApiKeyStage = value.tangemApiKeyStage,
|
||||
yieldModuleApiKey = value.yieldModuleApiKey,
|
||||
yieldModuleApiKeyDev = value.yieldModuleApiKeyDev,
|
||||
bffStaticToken = value.bffStaticToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ class EnvironmentConfigModel(
|
|||
@Json(name = "yieldModuleApiKeyDev") val yieldModuleApiKeyDev: String?,
|
||||
@Json(name = "blinkApiKey") val blinkApiKey: String?,
|
||||
@Json(name = "tatumApiKey") val tatumApiKey: String?,
|
||||
@Json(name = "bffStaticToken") val bffStaticToken: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_K
|
|||
import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_APPLICATION_STOPPED_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* All preferences keys that DataStore<Preferences> is stored.
|
||||
|
|
@ -176,6 +177,9 @@ object PreferencesKeys {
|
|||
fun getTangemPayAddToWalletKey(customerWalletAddress: String) =
|
||||
booleanPreferencesKey("tangem_pay_add_to_wallet_done_key_$customerWalletAddress")
|
||||
|
||||
fun getTangemPayCheckCustomerByWalletId(userWalletId: UserWalletId) =
|
||||
booleanPreferencesKey("tangem_pay_check_customer_by_wallet_id_$userWalletId")
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
|
||||
interface TangemPayStorage {
|
||||
|
||||
suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String)
|
||||
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String?
|
||||
|
||||
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens)
|
||||
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: TangemPayAuthTokens)
|
||||
|
||||
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
|
||||
suspend fun getAuthTokens(customerWalletAddress: String): TangemPayAuthTokens?
|
||||
|
||||
suspend fun storeOrderId(customerWalletAddress: String, orderId: String)
|
||||
|
||||
|
|
@ -21,6 +21,8 @@ interface TangemPayStorage {
|
|||
suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean
|
||||
|
||||
suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean)
|
||||
suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId)
|
||||
suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean?
|
||||
|
||||
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
|
||||
}
|
||||
|
|
@ -74,6 +74,7 @@ class ApiConfigTest {
|
|||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
|
||||
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
|
||||
ApiConfig.ID.TangemPayAuth -> TangemPayAuth(appVersionProvider = mockk())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
|
||||
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
|
||||
ApiConfig.ID.TangemPayAuth -> TangemPayAuth(appVersionProvider = appVersionProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,6 +131,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
ApiConfig.ID.MoonPay -> createMoonPayModel()
|
||||
ApiConfig.ID.P2PEthPool -> createP2PModel()
|
||||
ApiConfig.ID.News -> createNewsModel()
|
||||
ApiConfig.ID.TangemPayAuth -> createTangemPayAuthModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -257,6 +259,20 @@ internal class ProdApiConfigsManagerTest {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createTangemPayAuthModel(): TestModel {
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.TangemPayAuth,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "https://api.dev.us.paera.com/",
|
||||
headers = mapOf(
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createBlockAidSdkModel(): TestModel {
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.BlockAid,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
<string name="account_unsaved_dialog_message_create">Bist Du sicher, dass Du das neue Konto verwerfen willst?</string>
|
||||
<string name="account_unsaved_dialog_message_edit">Bist Du sicher, dass Du die Bearbeitungen verwerfen willst?</string>
|
||||
<string name="account_unsaved_dialog_title">Nicht gespeicherte Änderungen</string>
|
||||
<string name="accounts_migration_alert_message">Einige benutzerdefinierte Token wurden von „%1$s\" Zu \"%2$sda ihre Herleitung zu diesem Konto gehört.</string>
|
||||
<string name="accounts_migration_alert_message">Einige benutzerdefinierte Token wurden von “%1$s” Zu “%2$s” da ihre Herleitung zu diesem Konto gehört.</string>
|
||||
<string name="accounts_migration_alert_title">Einige benutzerdefinierte Token wurden verschoben.</string>
|
||||
<string name="action_buttons_buy_empty_search_message">Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum kaufen im Portfolio hinzu.</string>
|
||||
<string name="action_buttons_sell_empty_search_message">Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum verkaufen im Portfolio hinzu.</string>
|
||||
|
|
|
|||
|
|
@ -652,6 +652,8 @@
|
|||
<string name="no_account_polkadot">La cuenta de destino no está activa. Envíe %s o más para activar la cuenta.</string>
|
||||
<string name="no_account_send_to_create">Para crear una cuenta, envíe fondos a esta dirección</string>
|
||||
<string name="no_trustline_xlm_asset">La cuenta de destino no tiene una Trustline (línea de confianza) para el activo que se envía.</string>
|
||||
<string name="notification_black_friday_text">Obtén $10 en BTC con cada billetera \n ¡Date prisa!</string>
|
||||
<string name="notification_black_friday_title">Black Friday: hasta 30% DESCUENTO</string>
|
||||
<string name="notification_referral_promo_button">Únase ahora</string>
|
||||
<string name="notification_referral_promo_text">Comparta su código y gane 5 USDT por venta. Su amigo obtiene un 10% de descuento.</string>
|
||||
<string name="notification_referral_promo_title">¡Obtenga RECOMPENSAS por cada amigo!</string>
|
||||
|
|
|
|||
|
|
@ -1386,7 +1386,7 @@
|
|||
<string name="wc_alert_unknown_error_description">Code d\'erreur : %s. Si le problème persiste, n\'hésitez pas à contacter notre service d\'assistance.</string>
|
||||
<string name="wc_alert_unknown_error_description_no_error_code">Si le problème persiste, n’hésitez pas à contacter notre support.</string>
|
||||
<string name="wc_alert_unknown_error_title">Nous avons rencontré une erreur inconnue.</string>
|
||||
<string name="wc_alert_unsupported_dapps_description">Le portefeuille Tangem ne prend actuellement pas en charge %ss</string>
|
||||
<string name="wc_alert_unsupported_dapps_description">Le portefeuille Tangem ne prend actuellement pas en charge %s</string>
|
||||
<string name="wc_alert_unsupported_dapps_title">dApp non prise en charge</string>
|
||||
<string name="wc_alert_unsupported_method_description">Code d\'erreur : 8 005. Si le problème persiste, n\'hésitez pas à contacter notre support.</string>
|
||||
<string name="wc_alert_unsupported_method_title">Nous avons rencontré une erreur inconnue</string>
|
||||
|
|
|
|||
|
|
@ -138,10 +138,17 @@
|
|||
<string name="balance_hidden_title">残高は非表示</string>
|
||||
<string name="beta_mode_warning_message">ブロックチェーン開発者によると、Kaspaトークンは現在ベータ版です。アップデートをお楽しみに!</string>
|
||||
<string name="beta_mode_warning_title">ベータモード</string>
|
||||
<string name="biometric_disabled_warning_description">デバイスで生体認証がオフになっているため、ウォレットのロック解除に使用できません。\n生体認証を再度利用するには、デバイスの設定で有効にしてください。</string>
|
||||
<string name="biometric_disabled_warning_title">生体認証が無効になっています</string>
|
||||
<string name="biometric_lockout_permanent_warning_description">カードまたはリングをスキャンしてください</string>
|
||||
<string name="biometric_lockout_permanent_warning_description_2">生体認証の試行回数が上限に達しました。端末タップまたはアクセスコードでウォレットを解除してください。</string>
|
||||
<string name="biometric_lockout_permanent_warning_title">生体認証がロックされています</string>
|
||||
<string name="biometric_lockout_warning_description">30秒後に再試行するか、カードまたはリングをスキャンしてください</string>
|
||||
<string name="biometric_lockout_warning_description_2">生体認証ログインは一時的にロックされています。30秒後にもう一度お試しいただくか、端末タップまたはアクセスコードでウォレットを解除してください。</string>
|
||||
<string name="biometric_lockout_warning_title">試行回数が多すぎます</string>
|
||||
<string name="biometric_unavailable_warning">お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。</string>
|
||||
<string name="biometric_updated_warning_description">デバイスの生体認証が更新されました。再び生体認証ログインを有効にするため、ウォレットを選び、アクセスコードを入力してください。</string>
|
||||
<string name="biometric_updated_warning_title">対応が必要です</string>
|
||||
<string name="bitcoin_promo_activation_error">プロモーションコードの処理中にエラーが発生しました。しばらくしてからもう一度お試しください。</string>
|
||||
<string name="bitcoin_promo_activation_error_title">アクティベーションエラー</string>
|
||||
<string name="bitcoin_promo_activation_success">プロモーションコードが正常に有効化されました。報酬は14日以内にビットコインアカウントに入金されます。</string>
|
||||
|
|
@ -818,6 +825,8 @@
|
|||
<string name="no_account_polkadot">送信先アカウントが有効ではありません。%s 以上を送信してアカウントを有効にしてください。</string>
|
||||
<string name="no_account_send_to_create">アカウントを作成するには、このアドレスに資金を送金してください</string>
|
||||
<string name="no_trustline_xlm_asset">送信先アカウントには、送金されるアセットのトラストラインがありません。</string>
|
||||
<string name="notification_black_friday_text">ウォレットごとに$10相当のBTCをプレゼント\nお早めに!</string>
|
||||
<string name="notification_black_friday_title">ブラックフライデー:最大30% オフ</string>
|
||||
<string name="notification_referral_promo_button">今すぐ参加</string>
|
||||
<string name="notification_referral_promo_text">コードを共有すると、販売ごとに5 USDTを獲得できます。お友達は10%割引になります。</string>
|
||||
<string name="notification_referral_promo_title">友達への紹介で報酬を獲得しよう!</string>
|
||||
|
|
@ -1177,7 +1186,7 @@
|
|||
<string name="staking_amount_requirement_error">ステーキング金額は %s 以上である必要があります</string>
|
||||
<string name="staking_amount_tron_integer_error">ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。</string>
|
||||
<string name="staking_amount_tron_integer_error_unstaking">ネットワークルールにより、ステーキング解除の量は%1$s TRX に切り上げられます。</string>
|
||||
<string name="staking_apr_earn_badge">APR %1$s%</string>
|
||||
<string name="staking_apr_earn_badge">APR %1$s%%</string>
|
||||
<string name="staking_claim_unstaked">ステーキング解除分を請求する</string>
|
||||
<string name="staking_details_account_fee">ステーキングアカウント手数料</string>
|
||||
<string name="staking_details_account_fee_info">ステーキングアカウントとは、ステーキングされたSOLが保管される特別なアカウントです。トークンをバリデーターに委任し、取引の検証に参加して報酬を受け取る際に作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、ステーキング完了後に返金されます。</string>
|
||||
|
|
@ -1869,7 +1878,7 @@
|
|||
<string name="yield_module_fee_policy_sheet_fee_note">今後の追加入金ごとにおおよそのネットワーク手数料%1$s ( %2$s ) が差し引かれ、 %3$s ( %4$s ) の制限を超えることはありません。</string>
|
||||
<string name="yield_module_fee_policy_sheet_max_fee_note">ネットワーク手数料が上限を超えた場合、手数料が下がるまで取引は成立しません。この制限は後で変更できます。</string>
|
||||
<string name="yield_module_fee_policy_sheet_max_fee_title">最大手数料</string>
|
||||
<string name="yield_module_fee_policy_sheet_min_amount_note">最小金額は現在のネットワーク手数料に基づいて計算され、入金額の4% 、つまり最小の%1$s ( %2$s ) を超えないようにします。</string>
|
||||
<string name="yield_module_fee_policy_sheet_min_amount_note">最小金額は現在のネットワーク手数料に基づいて計算され、入金額の4%% 、つまり最小の%1$s ( %2$s ) を超えないようにします。</string>
|
||||
<string name="yield_module_fee_policy_sheet_min_amount_title">最低入金額</string>
|
||||
<string name="yield_module_fee_policy_sheet_title">入金手数料ポリシー</string>
|
||||
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangemは、生成された利息に対して15%サービス手数料も徴収します。</string>
|
||||
|
|
|
|||
|
|
@ -831,6 +831,8 @@
|
|||
<string name="no_account_polkadot">Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта.</string>
|
||||
<string name="no_account_send_to_create">Для создания аккаунта отправьте средства на этот адрес</string>
|
||||
<string name="no_trustline_xlm_asset">Аккаунт получателя не содержит трастлайна для отправляемого актива.</string>
|
||||
<string name="notification_black_friday_text">Получите $10 в BTC за каждый кошелёк\nПоторопитесь!</string>
|
||||
<string name="notification_black_friday_title">Чёрная пятница: скидки до 30%</string>
|
||||
<string name="notification_referral_promo_button">Присоединиться</string>
|
||||
<string name="notification_referral_promo_text">Поделись промокодом — заработай 5 USDT с каждой покупки. Твои друзья получат скидку 10% на карту Tangem!</string>
|
||||
<string name="notification_referral_promo_title">Получай бонусы за каждого друга!</string>
|
||||
|
|
|
|||
|
|
@ -842,8 +842,8 @@
|
|||
<string name="no_account_polkadot">Destination account is not active. Send %s or more to activate the account.</string>
|
||||
<string name="no_account_send_to_create">To create account send funds to this address</string>
|
||||
<string name="no_trustline_xlm_asset">The destination account does not have a trustline for the asset being sent.</string>
|
||||
<string name="notification_black_friday_text">Plus Rewards in BTC per Set.\nHurry up</string>
|
||||
<string name="notification_black_friday_title">Black Friday: up to 25% OFF</string>
|
||||
<string name="notification_black_friday_text">Get $10 in BTC with every wallet\nHurry!</string>
|
||||
<string name="notification_black_friday_title">Black Friday: up to 30% OFF</string>
|
||||
<string name="notification_referral_promo_button">Join Now</string>
|
||||
<string name="notification_referral_promo_text">Share your code - earn 5 USDT per sale. Your friend gets 10% OFF.</string>
|
||||
<string name="notification_referral_promo_title">Get REWARDS for every friend!</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.core.ui.components.inputrow
|
|||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -15,6 +16,7 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -35,7 +37,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
* @param modifier modifier
|
||||
* @param caption caption text
|
||||
* @param imageUrl icon to load
|
||||
* @param iconRes icon resource
|
||||
* @param iconResVector icon resource
|
||||
* @param subtitleColor subtitle text color
|
||||
* @param captionColor caption text color
|
||||
* @param iconTint icon tint
|
||||
|
|
@ -47,12 +49,13 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun InputRowImageBase(
|
||||
fun InputRowImageBase(
|
||||
subtitle: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
caption: TextReference? = null,
|
||||
imageUrl: String? = null,
|
||||
@DrawableRes iconRes: Int? = null,
|
||||
@DrawableRes iconResVector: Int? = null,
|
||||
@DrawableRes iconResWebp: Int? = null,
|
||||
subtitleColor: Color = TangemTheme.colors.text.primary1,
|
||||
captionColor: Color = TangemTheme.colors.text.tertiary,
|
||||
iconTint: Color = TangemTheme.colors.icon.informative,
|
||||
|
|
@ -76,7 +79,7 @@ internal fun InputRowImageBase(
|
|||
.clip(TangemTheme.shapes.roundedCornersXLarge),
|
||||
)
|
||||
SpacerW12()
|
||||
} else if (iconRes != null) {
|
||||
} else if (iconResVector != null) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
|
|
@ -85,13 +88,20 @@ internal fun InputRowImageBase(
|
|||
.background(iconTint.copy(alpha = 0.08f)),
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = ImageVector.vectorResource(id = iconRes)),
|
||||
painter = rememberVectorPainter(image = ImageVector.vectorResource(id = iconResVector)),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size18),
|
||||
)
|
||||
}
|
||||
SpacerW12()
|
||||
} else if (iconResWebp != null) {
|
||||
Image(
|
||||
painter = painterResource(iconResWebp),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size36),
|
||||
)
|
||||
SpacerW12()
|
||||
}
|
||||
Column {
|
||||
Row {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ fun InputRowImageInfo(
|
|||
subtitle = subtitle,
|
||||
caption = caption,
|
||||
imageUrl = imageUrl,
|
||||
iconRes = iconRes,
|
||||
iconResVector = iconRes,
|
||||
iconTint = iconTint,
|
||||
subtitleColor = subtitleColor,
|
||||
captionColor = captionColor,
|
||||
|
|
|
|||
BIN
core/ui/src/main/res/drawable/img_visa_36.webp
Normal file
BIN
core/ui/src/main/res/drawable/img_visa_36.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
implementation(projects.domain.visa)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
|
|
@ -31,6 +32,7 @@ dependencies {
|
|||
implementation(projects.domain.networks)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.quotes)
|
||||
implementation(projects.domain.common)
|
||||
|
||||
/** Feature API - remove after removing [HotWalletFeatureToggles] */
|
||||
implementation(projects.features.hotWallet.api)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import arrow.core.raise.either
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import com.tangem.domain.visa.model.TangemPayInitialCredentials
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -16,18 +16,14 @@ internal class DefaultTangemPayAuthDataSource @Inject constructor(
|
|||
) : TangemPayAuthDataSource {
|
||||
|
||||
override suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials> {
|
||||
val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId)
|
||||
|
||||
return when (initialCredentials) {
|
||||
return when (val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId)) {
|
||||
is CompletionResult.Failure<*> -> Either.Left(initialCredentials.error)
|
||||
is CompletionResult.Success<TangemPayInitialCredentials> -> Either.Right(initialCredentials.data)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, VisaAuthTokens> = either {
|
||||
visaAuthRemoteDataSource.refreshCustomerWalletAuthTokens(
|
||||
VisaAuthTokens.RefreshToken(refreshToken, authType = VisaAuthTokens.RefreshToken.Type.CardWallet),
|
||||
)
|
||||
override suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, TangemPayAuthTokens> = either {
|
||||
visaAuthRemoteDataSource.refreshCustomerWalletAuthTokens(refreshToken = refreshToken)
|
||||
.mapLeft { IllegalStateException("TangemPay token refresh failed. Error code: ${it.errorCode}") }
|
||||
.bind()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,22 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.KycStartInfo
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "TangemPay: KycRepository"
|
||||
|
||||
internal class DefaultKycRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
) : KycRepository {
|
||||
|
||||
override suspend fun getKycStartInfo(): Either<UniversalError, KycStartInfo> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.getKycAccess(authHeader = authHeader)
|
||||
}.result
|
||||
|
||||
KycStartInfo(token = result.token, locale = result.locale)
|
||||
}
|
||||
override suspend fun getKycStartInfo(userWalletId: UserWalletId): Either<VisaApiError, KycStartInfo> {
|
||||
return requestHelper.performRequest(
|
||||
userWalletId,
|
||||
) { authHeader -> tangemPayApi.getKycAccess(authHeader = authHeader) }
|
||||
.map { KycStartInfo(token = it.result.token, locale = it.result.locale) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,29 @@
|
|||
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.TangemPayCardFrozenStateStore
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
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
|
||||
import com.tangem.domain.pay.model.MainScreenCustomerInfo
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val VALID_STATUS = "valid"
|
||||
|
|
@ -34,105 +37,82 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
private val requestHelper: TangemPayRequestPerformer,
|
||||
private val tangemPayStorage: TangemPayStorage,
|
||||
private val authDataSource: TangemPayAuthDataSource,
|
||||
private val tangemPayWalletsManager: TangemPayWalletsManager,
|
||||
private val cardFrozenStateStore: TangemPayCardFrozenStateStore,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) : OnboardingRepository {
|
||||
|
||||
// Save data for a session
|
||||
private var lastFetchedCustomerInfo: CustomerInfo? = null
|
||||
private val lastFetchedCustomerInfoMap = ConcurrentHashMap<UserWalletId, CustomerInfo>()
|
||||
|
||||
override suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val result = tangemPayApi.validateDeeplink(DeeplinkValidityRequest(link))
|
||||
.getOrThrow()
|
||||
.result
|
||||
result?.status == VALID_STATUS
|
||||
override suspend fun validateDeeplink(link: String): Either<VisaApiError, Boolean> {
|
||||
return requestHelper.performWithStaticToken {
|
||||
tangemPayApi.validateDeeplink(body = DeeplinkValidityRequest(link = link))
|
||||
}.map { response ->
|
||||
response.result?.status == VALID_STATUS
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isTangemPayInitialDataProduced(): Boolean {
|
||||
val walletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId
|
||||
val customerWalletAddress = tangemPayStorage.getCustomerWalletAddress(walletId) ?: return false
|
||||
tangemPayStorage.getAuthTokens(customerWalletAddress) ?: return false
|
||||
override suspend fun isTangemPayInitialDataProduced(userWalletId: UserWalletId): Boolean {
|
||||
return withContext(dispatcherProvider.io) {
|
||||
val customerWalletAddress =
|
||||
tangemPayStorage.getCustomerWalletAddress(userWalletId) ?: return@withContext false
|
||||
tangemPayStorage.getAuthTokens(customerWalletAddress) ?: return@withContext false
|
||||
|
||||
return true
|
||||
return@withContext 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 },
|
||||
override suspend fun produceInitialData(userWalletId: UserWalletId) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
val initialCredentials = authDataSource.produceInitialCredentials(cardId = getCardId(userWalletId))
|
||||
.fold(
|
||||
ifLeft = { error -> error("Can not produce initial data: ${error.message}") },
|
||||
ifRight = { it },
|
||||
)
|
||||
// should storeCheckCustomerWalletResult because we already know this
|
||||
tangemPayStorage.storeCheckCustomerWalletResult(userWalletId)
|
||||
tangemPayStorage.storeCustomerWalletAddress(
|
||||
userWalletId = userWalletId,
|
||||
customerWalletAddress = initialCredentials.customerWalletAddress,
|
||||
)
|
||||
tangemPayStorage.storeAuthTokens(
|
||||
customerWalletAddress = initialCredentials.customerWalletAddress,
|
||||
tokens = initialCredentials.authTokens,
|
||||
)
|
||||
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 ->
|
||||
tangemPayApi.getCustomerMe(authHeader)
|
||||
}.result
|
||||
getCustomerInfo(result)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMainScreenCustomerInfo(): Either<UniversalError, MainScreenCustomerInfo> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val customerWalletAddress = requestHelper.getCustomerWalletAddress()
|
||||
override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo> {
|
||||
// TODO implement selector
|
||||
return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getCustomerMe(authHeader) }
|
||||
.map { response -> getCustomerInfo(userWalletId = userWalletId, response = response.result) }
|
||||
}
|
||||
|
||||
when (val orderId = tangemPayStorage.getOrderId(customerWalletAddress)) {
|
||||
// If order id wasn't saved -> start order creation and get customer info
|
||||
null -> {
|
||||
createOrder()
|
||||
MainScreenCustomerInfo(
|
||||
info = getCustomerInfoWithPersistedToken(),
|
||||
orderStatus = OrderStatus.UNKNOWN,
|
||||
)
|
||||
}
|
||||
// If order id was saved -> check its status
|
||||
else -> {
|
||||
val orderStatus = getOrderStatus(orderId)
|
||||
if (orderStatus == OrderStatus.CANCELED) {
|
||||
// If order was cancelled -> start order creation
|
||||
createOrder()
|
||||
}
|
||||
val customerInfo = when (orderStatus) {
|
||||
// Kyc is passed and user waits for order creation -> no need to get customer info
|
||||
OrderStatus.NEW,
|
||||
OrderStatus.PROCESSING,
|
||||
-> CustomerInfo(productInstance = null, isKycApproved = true, cardInfo = null)
|
||||
|
||||
// Order was created/cancelled -> clear order id and get customer info
|
||||
OrderStatus.UNKNOWN,
|
||||
OrderStatus.COMPLETED,
|
||||
OrderStatus.CANCELED,
|
||||
-> getCustomerInfoWithPersistedToken().also {
|
||||
tangemPayStorage.clearOrderId(customerWalletAddress)
|
||||
}
|
||||
}
|
||||
MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus)
|
||||
}
|
||||
}
|
||||
override suspend fun clearOrderId(userWalletId: UserWalletId) {
|
||||
withContext(dispatcherProvider.io) {
|
||||
val customerWalletAddress = requestHelper.getCustomerWalletAddress(userWalletId)
|
||||
tangemPayStorage.clearOrderId(customerWalletAddress = customerWalletAddress)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSavedCustomerInfo(): CustomerInfo? {
|
||||
return lastFetchedCustomerInfo
|
||||
override suspend fun getOrderId(userWalletId: UserWalletId): String? {
|
||||
return withContext(dispatcherProvider.io) {
|
||||
val customerWalletAddress = requestHelper.getCustomerWalletAddress(userWalletId)
|
||||
tangemPayStorage.getOrderId(customerWalletAddress)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createOrder() = withContext(dispatcherProvider.io) {
|
||||
override fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo? {
|
||||
return lastFetchedCustomerInfoMap[userWalletId]
|
||||
}
|
||||
|
||||
override suspend fun createOrder(userWalletId: UserWalletId) = withContext(dispatcherProvider.io) {
|
||||
launch {
|
||||
requestHelper.runWithErrorLogs(TAG) {
|
||||
val walletAddress = requestHelper.getCustomerWalletAddress()
|
||||
val result = requestHelper.request { authHeader ->
|
||||
val walletAddress = requestHelper.getCustomerWalletAddress(userWalletId)
|
||||
val result = requestHelper.request(userWalletId) { authHeader ->
|
||||
tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress))
|
||||
}.result ?: error("Create order result is null")
|
||||
|
||||
|
|
@ -141,7 +121,23 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getCustomerInfo(response: CustomerMeResponse.Result?): CustomerInfo {
|
||||
private fun getCardId(userWalletId: UserWalletId): String {
|
||||
val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId }
|
||||
} else {
|
||||
userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId }
|
||||
} ?: error("no userWallet found")
|
||||
return if (userWallet is UserWallet.Cold) {
|
||||
userWallet.cardId
|
||||
} else {
|
||||
TODO("[REDACTED_JIRA]")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCustomerInfo(
|
||||
userWalletId: UserWalletId,
|
||||
response: CustomerMeResponse.Result?,
|
||||
): CustomerInfo {
|
||||
val card = response?.card
|
||||
val balance = response?.balance
|
||||
val paymentAccount = response?.paymentAccount
|
||||
|
|
@ -157,46 +153,59 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
null
|
||||
}
|
||||
val productInstance = response?.productInstance?.let { instance ->
|
||||
cardFrozenStateStore.store(
|
||||
key = instance.cardId,
|
||||
value = when (instance.status) {
|
||||
CustomerMeResponse.ProductInstance.Status.ACTIVE -> TangemPayCardFrozenState.Unfrozen
|
||||
else -> TangemPayCardFrozenState.Frozen
|
||||
},
|
||||
)
|
||||
ProductInstance(
|
||||
id = instance.id,
|
||||
cardId = instance.cardId,
|
||||
status = when (instance.status) {
|
||||
CustomerMeResponse.ProductInstance.Status.ACTIVE -> ProductInstance.Status.ACTIVE
|
||||
else -> ProductInstance.Status.INACTIVE
|
||||
},
|
||||
)
|
||||
val cardFrozenState = when (instance.status) {
|
||||
CustomerMeResponse.ProductInstance.Status.ACTIVE -> TangemPayCardFrozenState.Unfrozen
|
||||
else -> TangemPayCardFrozenState.Frozen
|
||||
}
|
||||
cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState)
|
||||
|
||||
ProductInstance(id = instance.id, cardId = instance.cardId, cardFrozenState = cardFrozenState)
|
||||
}
|
||||
return CustomerInfo(
|
||||
productInstance = productInstance,
|
||||
isKycApproved = response?.kyc?.status == APPROVED_KYC_STATUS,
|
||||
cardInfo = cardInfo,
|
||||
).also { lastFetchedCustomerInfo = it }
|
||||
}
|
||||
|
||||
private suspend fun getOrderStatus(orderId: String): OrderStatus {
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.getOrder(authHeader, orderId)
|
||||
}.result ?: error("Order result is null")
|
||||
|
||||
return when (result.status) {
|
||||
OrderStatus.NEW.apiName -> OrderStatus.NEW
|
||||
OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING
|
||||
OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED
|
||||
else -> OrderStatus.CANCELED
|
||||
).also {
|
||||
lastFetchedCustomerInfoMap[userWalletId] = it
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCustomerInfoWithPersistedToken(): CustomerInfo {
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.getCustomerMe(authHeader)
|
||||
}.result
|
||||
return getCustomerInfo(result)
|
||||
override suspend fun getOrderStatus(
|
||||
userWalletId: UserWalletId,
|
||||
orderId: String,
|
||||
): Either<VisaApiError, OrderStatus> {
|
||||
return requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId)
|
||||
}.map { response ->
|
||||
when (response.result?.status) {
|
||||
null -> OrderStatus.UNKNOWN
|
||||
OrderStatus.NEW.apiName -> OrderStatus.NEW
|
||||
OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING
|
||||
OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED
|
||||
else -> OrderStatus.CANCELED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean> {
|
||||
val hasTangemPay = tangemPayStorage.checkCustomerWalletResult(userWalletId)
|
||||
if (hasTangemPay == true) {
|
||||
return Either.Right(true)
|
||||
}
|
||||
|
||||
return requestHelper.performWithStaticToken { staticToken ->
|
||||
tangemPayApi.checkCustomerWalletId(
|
||||
authHeader = staticToken,
|
||||
customerWalletId = userWalletId.stringValue,
|
||||
)
|
||||
}.map { response ->
|
||||
val id = response.id
|
||||
if (!id.isNullOrEmpty()) {
|
||||
tangemPayStorage.storeCheckCustomerWalletResult(userWalletId = userWalletId)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import com.tangem.datasource.api.pay.models.request.SetPinRequest
|
|||
import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse
|
||||
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
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
|
||||
|
|
@ -35,9 +36,9 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
private val cardFrozenStateStore: TangemPayCardFrozenStateStore,
|
||||
) : TangemPayCardDetailsRepository {
|
||||
|
||||
override suspend fun getCardBalance(): Either<UniversalError, TangemPayCardBalance> {
|
||||
override suspend fun getCardBalance(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardBalance> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val result = requestHelper.request { authHeader ->
|
||||
val result = requestHelper.request(userWalletId) { authHeader ->
|
||||
tangemPayApi.getCardBalance(authHeader)
|
||||
}.result ?: error("Cannot get card balance")
|
||||
TangemPayCardBalance(
|
||||
|
|
@ -47,12 +48,12 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun revealCardDetails(): Either<UniversalError, TangemPayCardDetails> {
|
||||
override suspend fun revealCardDetails(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardDetails> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val publicKeyBase64 = getPublicKeyBase64()
|
||||
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
|
||||
|
||||
val result = requestHelper.request { authHeader ->
|
||||
val result = requestHelper.request(userWalletId) { authHeader ->
|
||||
tangemPayApi.revealCardDetails(
|
||||
authHeader = authHeader,
|
||||
body = CardDetailsRequest(sessionId = sessionId),
|
||||
|
|
@ -81,14 +82,14 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun setPin(pin: String): Either<UniversalError, SetPinResult> {
|
||||
override suspend fun setPin(userWalletId: UserWalletId, pin: String): Either<UniversalError, SetPinResult> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val publicKeyBase64 = getPublicKeyBase64()
|
||||
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
|
||||
val encryptedData = rainCryptoUtil.encryptPin(pin = pin, secretKeyBytes = secretKeyBytes)
|
||||
secretKeyBytes.fill(0)
|
||||
|
||||
val status = requestHelper.request { authHeader ->
|
||||
val status = requestHelper.request(userWalletId) { authHeader ->
|
||||
tangemPayApi.setPin(
|
||||
authHeader = authHeader,
|
||||
body = SetPinRequest(
|
||||
|
|
@ -107,21 +108,24 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun isAddToWalletDone(): Either<UniversalError, Boolean> {
|
||||
override suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either<UniversalError, Boolean> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
storage.getAddToWalletDone(requestHelper.getCustomerWalletAddress())
|
||||
storage.getAddToWalletDone(requestHelper.getCustomerWalletAddress(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setAddToWalletAsDone(): Either<UniversalError, Unit> {
|
||||
override suspend fun setAddToWalletAsDone(userWalletId: UserWalletId): Either<UniversalError, Unit> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
storage.storeAddToWalletDone(requestHelper.getCustomerWalletAddress(), isDone = true)
|
||||
storage.storeAddToWalletDone(requestHelper.getCustomerWalletAddress(userWalletId), isDone = true)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun freezeCard(cardId: String): Either<UniversalError, TangemPayCardFrozenState> {
|
||||
override suspend fun freezeCard(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayCardFrozenState> {
|
||||
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending)
|
||||
return requestHelper.makeSafeRequest {
|
||||
return requestHelper.makeSafeRequest(userWalletId) {
|
||||
tangemPayApi.freezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
|
||||
}.onLeft {
|
||||
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Unfrozen)
|
||||
|
|
@ -141,9 +145,12 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun unfreezeCard(cardId: String): Either<UniversalError, TangemPayCardFrozenState> {
|
||||
override suspend fun unfreezeCard(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayCardFrozenState> {
|
||||
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending)
|
||||
return requestHelper.makeSafeRequest {
|
||||
return requestHelper.makeSafeRequest(userWalletId) {
|
||||
tangemPayApi.unfreezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId))
|
||||
}.onLeft {
|
||||
cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Frozen)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.data.visa.utils.TangemPayTxHistoryItemConverter
|
|||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListConfig
|
||||
|
|
@ -34,6 +35,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
|
|||
private val txHistoryItemConverter by lazy { TangemPayTxHistoryItemConverter(moshi) }
|
||||
|
||||
override fun getTxHistoryBatchFlow(
|
||||
userWalletId: UserWalletId,
|
||||
batchSize: Int,
|
||||
context: TangemPayTxHistoryListBatchingContext,
|
||||
): TangemPayTxHistoryListBatchFlow {
|
||||
|
|
@ -41,18 +43,24 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
|
|||
fetchDispatcher = dispatchers.io,
|
||||
context = context,
|
||||
generateNewKey = { keys -> keys.lastOrNull()?.inc() ?: 0 },
|
||||
batchFetcher = createFetcher(batchSize),
|
||||
batchFetcher = createFetcher(userWalletId, batchSize),
|
||||
).toBatchFlow()
|
||||
}
|
||||
|
||||
private fun createFetcher(
|
||||
userWalletId: UserWalletId,
|
||||
batchSize: Int,
|
||||
): BatchFetcher<TangemPayTxHistoryListConfig, List<TangemPayTxHistoryItem>> {
|
||||
return CursorBatchFetcher(
|
||||
prefetchDistance = batchSize,
|
||||
batchSize = batchSize,
|
||||
subFetcher = { request, _, _ ->
|
||||
val items = loadItems(config = request.params, cursor = request.cursor, limit = request.limit)
|
||||
val items = loadItems(
|
||||
userWalletId = userWalletId,
|
||||
config = request.params,
|
||||
cursor = request.cursor,
|
||||
limit = request.limit,
|
||||
)
|
||||
BatchFetchResult.Success(
|
||||
data = items,
|
||||
last = items.size < request.limit,
|
||||
|
|
@ -64,6 +72,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun loadItems(
|
||||
userWalletId: UserWalletId,
|
||||
config: TangemPayTxHistoryListConfig,
|
||||
cursor: String?,
|
||||
limit: Int,
|
||||
|
|
@ -71,7 +80,14 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
|
|||
cacheRegistry.invokeOnExpire(
|
||||
key = getCacheKey(customerWalletAddress = config.customerWalletAddress, cursor = cursor),
|
||||
skipCache = config.shouldRefresh,
|
||||
block = { fetch(customerWalletAddress = config.customerWalletAddress, cursor = cursor, pageSize = limit) },
|
||||
block = {
|
||||
fetch(
|
||||
userWalletId = userWalletId,
|
||||
customerWalletAddress = config.customerWalletAddress,
|
||||
cursor = cursor,
|
||||
pageSize = limit,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return txHistoryItemsStore.getSyncOrNull(
|
||||
|
|
@ -84,9 +100,14 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
|
|||
return "tangem_pay_tx_history_${customerWalletAddress}_${cursor ?: INITIAL_CURSOR}"
|
||||
}
|
||||
|
||||
private suspend fun fetch(customerWalletAddress: String, cursor: String?, pageSize: Int) {
|
||||
private suspend fun fetch(
|
||||
userWalletId: UserWalletId,
|
||||
customerWalletAddress: String,
|
||||
cursor: String?,
|
||||
pageSize: Int,
|
||||
) {
|
||||
requestPerformer.runWithErrorLogs(TAG) {
|
||||
val result = requestPerformer.request { authHeader ->
|
||||
val result = requestPerformer.request(userWalletId) { authHeader ->
|
||||
visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor)
|
||||
}.result
|
||||
val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull()
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.Either.Companion.catch
|
||||
import arrow.core.left
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.right
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.squareup.wire.Instant
|
||||
import com.tangem.data.pay.util.TangemPayErrorConverter
|
||||
import com.tangem.data.pay.util.TangemPayWalletsManager
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import com.tangem.domain.visa.model.getAuthHeader
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.*
|
||||
|
|
@ -25,20 +26,21 @@ import javax.inject.Inject
|
|||
|
||||
internal class TangemPayRequestPerformer @Inject constructor(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tangemPayStorage: TangemPayStorage,
|
||||
private val authDataSource: TangemPayAuthDataSource,
|
||||
private val tangemPayWalletsManager: TangemPayWalletsManager,
|
||||
) {
|
||||
|
||||
private val customerWalletAddress = MutableStateFlow<String?>(null)
|
||||
|
||||
private val refreshTokensMutex = Mutex()
|
||||
private var refreshTokensJob: Deferred<VisaAuthTokens>? = null
|
||||
private var refreshTokensJob: Deferred<TangemPayAuthTokens>? = null
|
||||
|
||||
private val errorConverter = TangemPayErrorConverter(moshi)
|
||||
|
||||
suspend fun <T : Any> runWithErrorLogs(tag: String, requestBlock: suspend () -> T): Either<UniversalError, T> {
|
||||
@Deprecated("Do not use this method")
|
||||
suspend fun <T : Any> runWithErrorLogs(tag: String, requestBlock: suspend () -> T): Either<VisaApiError, T> {
|
||||
return try {
|
||||
val result = requestBlock()
|
||||
Either.Right(result)
|
||||
|
|
@ -56,54 +58,81 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun <T : Any> makeSafeRequest(
|
||||
userWalletId: UserWalletId,
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
): Either<VisaApiError, T> {
|
||||
return catch { request(requestBlock) }
|
||||
.mapLeft { exception ->
|
||||
Timber.tag("TangemPayRequestPerformer").e(exception)
|
||||
errorConverter.convert(exception)
|
||||
}
|
||||
return performRequest(userWalletId, requestBlock)
|
||||
}
|
||||
|
||||
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> ApiResponse<T>): T =
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(
|
||||
requestBlock = requestBlock,
|
||||
getTokens = ::getAccessTokens,
|
||||
refreshTokens = ::refreshAuthTokens,
|
||||
@Deprecated("Use perform request instead", replaceWith = ReplaceWith("performRequest"))
|
||||
suspend fun <T : Any> request(
|
||||
userWalletId: UserWalletId,
|
||||
requestBlock: suspend (header: String) ->
|
||||
ApiResponse<T>,
|
||||
): T = withContext(dispatchers.io) {
|
||||
performRequest(userWalletId, requestBlock = requestBlock)
|
||||
// to keep behaviour as previous
|
||||
.fold(
|
||||
ifRight = { it },
|
||||
ifLeft = { error -> error("Cannot perform request: $error") },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> performRequest(
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
getTokens: (suspend () -> VisaAuthTokens),
|
||||
refreshTokens: (suspend () -> VisaAuthTokens)? = null,
|
||||
): T = runCatching {
|
||||
val tokens = getTokens()
|
||||
val header = tokens.getAuthHeader()
|
||||
requestBlock(header).getOrThrow()
|
||||
}.getOrElse { error ->
|
||||
val unauthorizedCode = ApiResponseError.HttpException.Code.UNAUTHORIZED
|
||||
if (error is ApiResponseError.HttpException && refreshTokens != null && error.code == unauthorizedCode) {
|
||||
refreshOrJoin(refreshTokens)
|
||||
performRequest(requestBlock, refreshTokens = null, getTokens = getTokens)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshOrJoin(refreshTokens: suspend () -> VisaAuthTokens): VisaAuthTokens {
|
||||
val jobToAwait: Deferred<VisaAuthTokens> =
|
||||
refreshTokensMutex.withLock {
|
||||
val current = refreshTokensJob
|
||||
if (current == null || current.isCompleted) {
|
||||
coroutineScope {
|
||||
async { refreshTokens() }.also { refreshTokensJob = it }
|
||||
}
|
||||
} else {
|
||||
current
|
||||
suspend fun <T : Any> performWithStaticToken(
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
): Either<VisaApiError, T> = withContext(dispatchers.io) {
|
||||
catch(
|
||||
block = {
|
||||
val staticToken =
|
||||
environmentConfigStorage.getConfigSync().bffStaticToken ?: error("BFF static token is null")
|
||||
when (val apiResponse = requestBlock(staticToken)) {
|
||||
is ApiResponse.Error -> errorConverter.convert(apiResponse.cause).left()
|
||||
is ApiResponse.Success<T> -> apiResponse.data.right()
|
||||
}
|
||||
},
|
||||
catch = { errorConverter.convert(it).left() },
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <T : Any> performRequest(
|
||||
userWalletId: UserWalletId,
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
): Either<VisaApiError, T> = withContext(dispatchers.io) {
|
||||
catch(
|
||||
block = {
|
||||
val tokens = getAccessTokens(userWalletId)
|
||||
val now = Instant.now()
|
||||
val accessExpiresAt = Instant.ofEpochSecond(tokens.expiresAt)
|
||||
val refreshExpiresAt = Instant.ofEpochSecond(tokens.refreshExpiresAt)
|
||||
val apiResponse: ApiResponse<T> = if (accessExpiresAt.isAfter(now)) {
|
||||
requestBlock(tokens.getAuthHeader())
|
||||
} else if (accessExpiresAt.isBefore(now) && refreshExpiresAt.isAfter(now)) {
|
||||
val newTokens = refreshOrJoin(refreshTokens = { refreshAuthTokens(userWalletId) })
|
||||
requestBlock(newTokens.getAuthHeader())
|
||||
} else {
|
||||
return@catch VisaApiError.RefreshTokenExpired.left()
|
||||
}
|
||||
|
||||
when (apiResponse) {
|
||||
is ApiResponse.Error -> errorConverter.convert(apiResponse.cause).left()
|
||||
is ApiResponse.Success<T> -> apiResponse.data.right()
|
||||
}
|
||||
},
|
||||
catch = { errorConverter.convert(it).left() },
|
||||
).onLeft { visaApiError -> Timber.tag("TangemPayRequestPerformer").e(visaApiError.toString()) }
|
||||
}
|
||||
|
||||
private suspend fun refreshOrJoin(refreshTokens: suspend () -> TangemPayAuthTokens): TangemPayAuthTokens {
|
||||
val jobToAwait: Deferred<TangemPayAuthTokens> = refreshTokensMutex.withLock {
|
||||
val current = refreshTokensJob
|
||||
if (current == null || current.isCompleted) {
|
||||
coroutineScope {
|
||||
async { refreshTokens() }.also { refreshTokensJob = it }
|
||||
}
|
||||
} else {
|
||||
current
|
||||
}
|
||||
}
|
||||
val result = try {
|
||||
jobToAwait.await()
|
||||
} finally {
|
||||
|
|
@ -116,28 +145,28 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
return result
|
||||
}
|
||||
|
||||
suspend fun getCustomerWalletAddress(): String {
|
||||
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String {
|
||||
val existingAddress = customerWalletAddress.value
|
||||
if (existingAddress != null) {
|
||||
return existingAddress
|
||||
}
|
||||
val storedAddress = tangemPayStorage.getCustomerWalletAddress(
|
||||
userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPay().walletId,
|
||||
userWalletId = userWalletId,
|
||||
) ?: error("Can not find customer address")
|
||||
|
||||
customerWalletAddress.value = storedAddress
|
||||
return storedAddress
|
||||
}
|
||||
|
||||
private suspend fun getAccessTokens(): VisaAuthTokens {
|
||||
val walletAddress = getCustomerWalletAddress()
|
||||
private suspend fun getAccessTokens(userWalletId: UserWalletId): TangemPayAuthTokens {
|
||||
val walletAddress = getCustomerWalletAddress(userWalletId)
|
||||
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
|
||||
private suspend fun refreshAuthTokens(userWalletId: UserWalletId): TangemPayAuthTokens {
|
||||
val customerWalletAddress = getCustomerWalletAddress(userWalletId)
|
||||
val refreshToken = getAccessTokens(userWalletId).refreshToken
|
||||
val tokens = authDataSource.refreshAuthTokens(refreshToken)
|
||||
.fold(
|
||||
ifLeft = { error -> error("Cannot refresh tokens: ${error.message}") },
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ class TangemPayErrorConverter(moshi: Moshi) : Converter<Throwable, VisaApiError>
|
|||
|
||||
override fun convert(value: Throwable): VisaApiError {
|
||||
return if (value is ApiResponseError.HttpException) {
|
||||
if (value.code == ApiResponseError.HttpException.Code.NOT_FOUND) return VisaApiError.NotPaeraCustomer
|
||||
|
||||
val errorBody = value.errorBody ?: return VisaApiError.UnknownWithoutCode
|
||||
return runCatching {
|
||||
visaErrorAdapter.fromJson(errorBody)?.error?.code ?: value.code.numericCode
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import kotlinx.coroutines.flow.filter
|
|||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class TangemPayWalletsManager @Inject constructor(
|
||||
// TODO remove after implement wallet selector in pay
|
||||
class TangemPayWalletsManager @Inject constructor(
|
||||
private val manager: UserWalletsListManager,
|
||||
private val repository: UserWalletsListRepository,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
|
|
|
|||
|
|
@ -5,15 +5,13 @@ import com.squareup.moshi.Moshi
|
|||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.pay.models.request.*
|
||||
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthSession
|
||||
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
|
@ -21,6 +19,7 @@ import javax.inject.Inject
|
|||
internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
private val visaAuthApi: TangemPayApi,
|
||||
private val tangemPayAuthApi: TangemPayAuthApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : VisaAuthRemoteDataSource {
|
||||
|
||||
|
|
@ -66,15 +65,19 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
|
||||
override suspend fun getCustomerWalletAuthChallenge(
|
||||
customerWalletAddress: String,
|
||||
customerWalletId: String,
|
||||
): Either<VisaApiError, VisaAuthChallenge.Wallet> = withContext(dispatchers.io) {
|
||||
request {
|
||||
visaAuthApi.generateNonceByCustomerWallet(
|
||||
GenerateNonceByCustomerWalletRequest(customerWalletAddress = customerWalletAddress),
|
||||
tangemPayAuthApi.generateNonceByCustomerWallet(
|
||||
request = GenerateNonceByCustomerWalletRequest(
|
||||
customerWalletAddress = customerWalletAddress,
|
||||
customerWalletId = customerWalletId,
|
||||
),
|
||||
).getOrThrow()
|
||||
}.map { response ->
|
||||
VisaAuthChallenge.Wallet(
|
||||
challenge = response.result.nonce,
|
||||
session = VisaAuthSession(response.result.sessionId),
|
||||
challenge = response.nonce,
|
||||
session = VisaAuthSession(response.sessionId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -83,37 +86,42 @@ internal class DefaultVisaAuthRemoteDataSource @Inject constructor(
|
|||
sessionId: String,
|
||||
signature: String,
|
||||
nonce: String,
|
||||
): Either<VisaApiError, VisaAuthTokens> = withContext(dispatchers.io) {
|
||||
): Either<VisaApiError, TangemPayAuthTokens> = withContext(dispatchers.io) {
|
||||
request {
|
||||
visaAuthApi.getTokenByCustomerWallet(
|
||||
GetTokenByCustomerWalletRequest(
|
||||
tangemPayAuthApi.getTokenByCustomerWallet(
|
||||
request = GetTokenByCustomerWalletRequest(
|
||||
authType = "customer_wallet",
|
||||
sessionId = sessionId,
|
||||
signature = signature,
|
||||
messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce",
|
||||
),
|
||||
).getOrThrow()
|
||||
}.map { response ->
|
||||
VisaAuthTokens(
|
||||
response.result.accessToken,
|
||||
VisaAuthTokens.RefreshToken(
|
||||
response.result.refreshToken,
|
||||
VisaAuthTokens.RefreshToken.Type.CardWallet,
|
||||
),
|
||||
TangemPayAuthTokens(
|
||||
accessToken = response.accessToken,
|
||||
expiresAt = response.expiresAt,
|
||||
refreshToken = response.refreshToken,
|
||||
refreshExpiresAt = response.refreshExpiresAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun refreshCustomerWalletAuthTokens(
|
||||
refreshToken: VisaAuthTokens.RefreshToken,
|
||||
): Either<VisaApiError, VisaAuthTokens> = withContext(dispatchers.io) {
|
||||
refreshToken: String,
|
||||
): Either<VisaApiError, TangemPayAuthTokens> = withContext(dispatchers.io) {
|
||||
request {
|
||||
visaAuthApi.refreshCustomerWalletAccessToken(
|
||||
RefreshCustomerWalletAccessTokenRequest(refreshToken = refreshToken.value),
|
||||
tangemPayAuthApi.refreshCustomerWalletAccessToken(
|
||||
request = RefreshCustomerWalletAccessTokenRequest(
|
||||
authType = "customer_wallet",
|
||||
refreshToken = refreshToken,
|
||||
),
|
||||
).getOrThrow()
|
||||
}.map { response ->
|
||||
VisaAuthTokens(
|
||||
accessToken = response.result.accessToken,
|
||||
refreshToken = refreshToken.copy(value = response.result.refreshToken),
|
||||
TangemPayAuthTokens(
|
||||
accessToken = response.accessToken,
|
||||
expiresAt = response.expiresAt,
|
||||
refreshToken = response.refreshToken,
|
||||
refreshExpiresAt = response.refreshExpiresAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ dependencies {
|
|||
implementation(deps.spongecastle.core)
|
||||
|
||||
/** Libs - Other */
|
||||
implementation(deps.timber)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
implementation(deps.moshi)
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ sealed class VisaApiError(
|
|||
fun isUnknown() = this is UnknownWithoutCode || this is Unknown
|
||||
|
||||
data object RefreshTokenExpired : VisaApiError(104004001)
|
||||
data object NotPaeraCustomer : VisaApiError(104004002)
|
||||
|
||||
companion object {
|
||||
fun fromBackendError(backendErrorCode: Int): VisaApiError {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TangemPayAuthTokens(
|
||||
@Json(name = "access_token") val accessToken: String,
|
||||
@Json(name = "expires_at") val expiresAt: Long,
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
@Json(name = "refresh_expires_at") val refreshExpiresAt: Long,
|
||||
)
|
||||
|
||||
fun TangemPayAuthTokens.getAuthHeader(): String {
|
||||
return "Bearer $accessToken"
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
package com.tangem.domain.visa.model
|
||||
|
||||
data class TangemPayInitialCredentials(val customerWalletAddress: String, val authTokens: VisaAuthTokens)
|
||||
data class TangemPayInitialCredentials(val customerWalletAddress: String, val authTokens: TangemPayAuthTokens)
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.domain.pay.datasource
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import com.tangem.domain.visa.model.TangemPayInitialCredentials
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
|
||||
interface TangemPayAuthDataSource {
|
||||
|
||||
suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials>
|
||||
|
||||
suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, VisaAuthTokens>
|
||||
suspend fun refreshAuthTokens(refreshToken: String): Either<Throwable, TangemPayAuthTokens>
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class MainScreenCustomerInfo(
|
||||
|
|
@ -16,13 +17,8 @@ data class CustomerInfo(
|
|||
data class ProductInstance(
|
||||
val id: String,
|
||||
val cardId: String,
|
||||
val status: Status,
|
||||
) {
|
||||
enum class Status {
|
||||
ACTIVE,
|
||||
INACTIVE,
|
||||
}
|
||||
}
|
||||
val cardFrozenState: TangemPayCardFrozenState,
|
||||
)
|
||||
|
||||
data class CardInfo(
|
||||
val lastFourDigits: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
sealed interface TangemPayCustomerInfoError {
|
||||
|
||||
data object UnavailableError : TangemPayCustomerInfoError
|
||||
data object RefreshNeededError : TangemPayCustomerInfoError
|
||||
data object UnknownError : TangemPayCustomerInfoError
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.KycStartInfo
|
||||
|
||||
interface KycRepository {
|
||||
|
|
@ -9,5 +10,5 @@ interface KycRepository {
|
|||
/**
|
||||
* Returns KYC data to start or continue the survey
|
||||
*/
|
||||
suspend fun getKycStartInfo(): Either<UniversalError, KycStartInfo>
|
||||
suspend fun getKycStartInfo(userWalletId: UserWalletId): Either<UniversalError, KycStartInfo>
|
||||
}
|
||||
|
|
@ -2,23 +2,27 @@ package com.tangem.domain.pay.repository
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.MainScreenCustomerInfo
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
interface OnboardingRepository {
|
||||
|
||||
suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean>
|
||||
|
||||
suspend fun isTangemPayInitialDataProduced(): Boolean
|
||||
suspend fun isTangemPayInitialDataProduced(userWalletId: UserWalletId): Boolean
|
||||
|
||||
suspend fun produceInitialData()
|
||||
suspend fun produceInitialData(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo>
|
||||
suspend fun getCustomerInfo(userWalletId: UserWalletId): Either<VisaApiError, CustomerInfo>
|
||||
|
||||
/**
|
||||
* Returns only if the user already authorised at least once
|
||||
*/
|
||||
suspend fun getMainScreenCustomerInfo(): Either<UniversalError, MainScreenCustomerInfo>
|
||||
suspend fun createOrder(userWalletId: UserWalletId): Job
|
||||
|
||||
fun getSavedCustomerInfo(): CustomerInfo?
|
||||
suspend fun clearOrderId(userWalletId: UserWalletId)
|
||||
suspend fun getOrderId(userWalletId: UserWalletId): String?
|
||||
suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either<VisaApiError, OrderStatus>
|
||||
suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either<VisaApiError, Boolean>
|
||||
fun getSavedCustomerInfo(userWalletId: UserWalletId): CustomerInfo?
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
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
|
||||
|
|
@ -10,18 +11,22 @@ import kotlinx.coroutines.flow.Flow
|
|||
|
||||
interface TangemPayCardDetailsRepository {
|
||||
|
||||
suspend fun getCardBalance(): Either<UniversalError, TangemPayCardBalance>
|
||||
suspend fun getCardBalance(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardBalance>
|
||||
|
||||
suspend fun revealCardDetails(): Either<UniversalError, TangemPayCardDetails>
|
||||
suspend fun revealCardDetails(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardDetails>
|
||||
|
||||
suspend fun setPin(pin: String): Either<UniversalError, SetPinResult>
|
||||
suspend fun setPin(userWalletId: UserWalletId, pin: String): Either<UniversalError, SetPinResult>
|
||||
|
||||
suspend fun isAddToWalletDone(): Either<UniversalError, Boolean>
|
||||
suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either<UniversalError, Boolean>
|
||||
|
||||
suspend fun setAddToWalletAsDone(): Either<UniversalError, Unit>
|
||||
suspend fun setAddToWalletAsDone(userWalletId: UserWalletId): Either<UniversalError, Unit>
|
||||
|
||||
suspend fun freezeCard(userWalletId: UserWalletId, cardId: String): Either<UniversalError, TangemPayCardFrozenState>
|
||||
suspend fun unfreezeCard(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayCardFrozenState>
|
||||
|
||||
suspend fun freezeCard(cardId: String): Either<UniversalError, TangemPayCardFrozenState>
|
||||
suspend fun unfreezeCard(cardId: String): Either<UniversalError, TangemPayCardFrozenState>
|
||||
fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState>
|
||||
suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState?
|
||||
}
|
||||
|
|
@ -2,20 +2,16 @@ package com.tangem.domain.pay.usecase
|
|||
|
||||
import arrow.core.Either
|
||||
import arrow.core.Either.Companion.catch
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
|
||||
class ProduceTangemPayInitialDataUseCase(
|
||||
private val repository: OnboardingRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): Either<Throwable, Unit> {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<Throwable, Unit> {
|
||||
return catch {
|
||||
val isDataProduced = repository.isTangemPayInitialDataProduced()
|
||||
if (isDataProduced) {
|
||||
return@catch Unit
|
||||
} else {
|
||||
repository.produceInitialData()
|
||||
}
|
||||
repository.produceInitialData(userWalletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,19 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.MainScreenCustomerInfo
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.TangemPayCustomerInfoError
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import timber.log.Timber
|
||||
|
||||
private const val TAG = "TangemPayMainScreenCustomerInfoUseCase"
|
||||
|
||||
/**
|
||||
* Returns tangem pay customer info for the main screen banner
|
||||
|
|
@ -11,5 +23,96 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
private val repository: OnboardingRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): MainScreenCustomerInfo? = repository.getMainScreenCustomerInfo().getOrNull()
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> = catch(
|
||||
block = {
|
||||
repository.checkCustomerWallet(userWalletId)
|
||||
.fold(
|
||||
ifLeft = { TangemPayCustomerInfoError.UnknownError.left() },
|
||||
ifRight = { hasTangemPay ->
|
||||
if (hasTangemPay) {
|
||||
proceedWithPaeraCustomerResult(userWalletId)
|
||||
} else {
|
||||
TangemPayCustomerInfoError.UnknownError.left() // ignore if there's no TangemPay
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
catch = { error ->
|
||||
Timber.tag(TAG).e(error)
|
||||
TangemPayCustomerInfoError.UnknownError.left()
|
||||
},
|
||||
)
|
||||
|
||||
private suspend fun proceedWithPaeraCustomerResult(
|
||||
userWalletId: UserWalletId,
|
||||
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
|
||||
val orderId = repository.getOrderId(userWalletId)
|
||||
return if (orderId != null) {
|
||||
proceedWithOrderId(userWalletId = userWalletId, orderId = orderId)
|
||||
} else {
|
||||
proceedWithoutOrder(userWalletId = userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithoutOrder(
|
||||
userWalletId: UserWalletId,
|
||||
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
|
||||
return repository.getCustomerInfo(userWalletId)
|
||||
.mapLeft { error -> error.mapErrorForCustomer() }
|
||||
.map { customerInfo ->
|
||||
if (customerInfo.cardInfo == null) {
|
||||
// If order id wasn't saved -> start order creation and get customer info
|
||||
repository.createOrder(userWalletId)
|
||||
}
|
||||
MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.UNKNOWN)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithOrderId(
|
||||
userWalletId: UserWalletId,
|
||||
orderId: String,
|
||||
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
|
||||
return repository.getOrderStatus(userWalletId, orderId = orderId)
|
||||
.fold(
|
||||
ifLeft = { error ->
|
||||
error.mapErrorForCustomer().left()
|
||||
},
|
||||
ifRight = { orderStatus ->
|
||||
when (orderStatus) {
|
||||
// Kyc is passed and user waits for order creation -> no need to get customer info
|
||||
OrderStatus.NEW,
|
||||
OrderStatus.PROCESSING,
|
||||
-> MainScreenCustomerInfo(
|
||||
info = CustomerInfo(productInstance = null, isKycApproved = true, cardInfo = null),
|
||||
orderStatus = orderStatus,
|
||||
).right()
|
||||
|
||||
// Order was created/cancelled -> clear order id and get customer info
|
||||
OrderStatus.COMPLETED,
|
||||
OrderStatus.CANCELED,
|
||||
OrderStatus.UNKNOWN,
|
||||
-> {
|
||||
repository.clearOrderId(userWalletId)
|
||||
// If order was cancelled -> start order creation
|
||||
if (orderStatus == OrderStatus.CANCELED) repository.createOrder(userWalletId)
|
||||
repository.getCustomerInfo(userWalletId = userWalletId)
|
||||
.mapLeft { it.mapErrorForCustomer() }
|
||||
.map { customerInfo ->
|
||||
MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun VisaApiError.mapErrorForCustomer(): TangemPayCustomerInfoError {
|
||||
return if (this !is VisaApiError.NotPaeraCustomer) {
|
||||
TangemPayCustomerInfoError.UnavailableError
|
||||
} else {
|
||||
TangemPayCustomerInfoError.UnknownError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.domain.tangempay.repository
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
|
||||
|
||||
interface TangemPayTxHistoryRepository {
|
||||
fun getTxHistoryBatchFlow(
|
||||
userWalletId: UserWalletId,
|
||||
batchSize: Int,
|
||||
context: TangemPayTxHistoryListBatchingContext,
|
||||
): TangemPayTxHistoryListBatchFlow
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.visa.datasource
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayAuthTokens
|
||||
import com.tangem.domain.visa.model.VisaAuthChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthSignedChallenge
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
|
|
@ -20,17 +21,16 @@ interface VisaAuthRemoteDataSource {
|
|||
|
||||
suspend fun getCustomerWalletAuthChallenge(
|
||||
customerWalletAddress: String,
|
||||
customerWalletId: String,
|
||||
): Either<VisaApiError, VisaAuthChallenge.Wallet>
|
||||
|
||||
suspend fun getTokenWithCustomerWallet(
|
||||
sessionId: String,
|
||||
signature: String,
|
||||
nonce: String,
|
||||
): Either<VisaApiError, VisaAuthTokens>
|
||||
): Either<VisaApiError, TangemPayAuthTokens>
|
||||
|
||||
suspend fun refreshCustomerWalletAuthTokens(
|
||||
refreshToken: VisaAuthTokens.RefreshToken,
|
||||
): Either<VisaApiError, VisaAuthTokens>
|
||||
suspend fun refreshCustomerWalletAuthTokens(refreshToken: String): Either<VisaApiError, TangemPayAuthTokens>
|
||||
|
||||
suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either<VisaApiError, VisaAuthTokens>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ package com.tangem.features.kyc
|
|||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface KycComponent : ComposableContentComponent {
|
||||
|
||||
data object Params
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
|
||||
interface Factory : ComponentFactory<Params, KycComponent>
|
||||
}
|
||||
|
|
@ -3,27 +3,36 @@ package com.tangem.features.kyc
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.pay.KycStartInfo
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
class DefaultKycModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val kycRepository: KycRepository,
|
||||
) : Model() {
|
||||
|
||||
private val params: KycComponent.Params = paramsContainer.require()
|
||||
|
||||
private val _uiState: MutableStateFlow<KycStartInfo?> = MutableStateFlow(null)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
kycRepository.getKycStartInfo().getOrNull()?.let { _uiState.emit(it) }
|
||||
try {
|
||||
kycRepository.getKycStartInfo(params.userWalletId).getOrNull()?.let { _uiState.emit(it) }
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ class DefaultTangemPayDetailsContainerComponent @AssistedInject constructor(
|
|||
)
|
||||
TangemPayDetailsInnerRoute.ChangePIN -> TangemPayChangePinComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = params,
|
||||
)
|
||||
TangemPayDetailsInnerRoute.ChangePINSuccess -> TangemPayChangePinSuccessComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@ import com.tangem.features.tangempay.ui.TangemPayChangePinScreen
|
|||
|
||||
internal class TangemPayChangePinComponent(
|
||||
private val appComponentContext: AppComponentContext,
|
||||
params: TangemPayDetailsContainerComponent.Params,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
private val model: TangemPayChangePinModel = getOrCreateModel()
|
||||
private val model: TangemPayChangePinModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ internal class TangemPayDetailsComponent(
|
|||
private val txHistoryComponent = DefaultTangemPayTxHistoryComponent(
|
||||
appComponentContext = child("txHistoryComponent"),
|
||||
params = DefaultTangemPayTxHistoryComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
customerWalletAddress = params.config.customerWalletAddress,
|
||||
uiActions = model,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.lazy.LazyListScope
|
|||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
|
||||
import com.tangem.features.tangempay.model.TangemPayTxHistoryModel
|
||||
import com.tangem.features.tangempay.ui.tangemPayTxHistoryItems
|
||||
|
|
@ -22,5 +23,9 @@ internal class DefaultTangemPayTxHistoryComponent(
|
|||
tangemPayTxHistoryItems(listState, state)
|
||||
}
|
||||
|
||||
data class Params(val customerWalletAddress: String, val uiActions: TangemPayTxHistoryUiActions)
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val customerWalletAddress: String,
|
||||
val uiActions: TangemPayTxHistoryUiActions,
|
||||
)
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
uiState.transformerUpdate(
|
||||
transformer = DetailsRevealProgressStateTransformer(onClickHide = ::hideCardDetails),
|
||||
)
|
||||
cardDetailsRepository.revealCardDetails()
|
||||
cardDetailsRepository.revealCardDetails(params.params.userWalletId)
|
||||
.onRight { cardDetails ->
|
||||
uiState.transformerUpdate(
|
||||
transformer = DetailsRevealedStateTransformer(
|
||||
|
|
|
|||
|
|
@ -3,28 +3,31 @@ package com.tangem.features.tangempay.model
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
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.model.SetPinResult
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayChangePinUM
|
||||
import kotlinx.coroutines.flow.*
|
||||
import com.tangem.features.tangempay.model.transformers.PinCodeChangeTransformer
|
||||
import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayChangePinModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
) : Model() {
|
||||
|
||||
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
|
||||
val uiState: StateFlow<TangemPayChangePinUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
|
|
@ -35,7 +38,15 @@ internal class TangemPayChangePinModel @Inject constructor(
|
|||
private fun onClickSubmit() {
|
||||
modelScope.launch {
|
||||
uiState.update { it.copy(submitButtonLoading = true) }
|
||||
val result = cardDetailsRepository.setPin(uiState.value.pinCode).getOrNull()
|
||||
val result = try {
|
||||
cardDetailsRepository.setPin(
|
||||
userWalletId = params.userWalletId,
|
||||
pin = uiState.value.pinCode,
|
||||
).getOrNull()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return@launch
|
||||
}
|
||||
uiState.update { it.copy(submitButtonLoading = false) }
|
||||
when (result) {
|
||||
SetPinResult.SUCCESS -> router.push(TangemPayDetailsInnerRoute.ChangePINSuccess)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -123,7 +124,13 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
|
||||
private fun freezeCard() {
|
||||
modelScope.launch {
|
||||
cardDetailsRepository.freezeCard(cardId = params.config.cardId)
|
||||
val result = try {
|
||||
cardDetailsRepository.freezeCard(userWalletId = params.userWalletId, cardId = params.config.cardId)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return@launch
|
||||
}
|
||||
result
|
||||
.onLeft {
|
||||
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_failed)))
|
||||
}
|
||||
|
|
@ -155,7 +162,13 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
|
||||
private fun unfreezeCard() {
|
||||
modelScope.launch {
|
||||
cardDetailsRepository.unfreezeCard(cardId = params.config.cardId)
|
||||
val result = try {
|
||||
cardDetailsRepository.unfreezeCard(userWalletId = params.userWalletId, cardId = params.config.cardId)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return@launch
|
||||
}
|
||||
result
|
||||
.onLeft {
|
||||
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_failed)))
|
||||
}
|
||||
|
|
@ -204,7 +217,12 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
|
||||
private fun fetchBalance(): Job {
|
||||
return modelScope.launch {
|
||||
val result = cardDetailsRepository.getCardBalance().onRight { balance = it }
|
||||
val result = try {
|
||||
cardDetailsRepository.getCardBalance(params.userWalletId).onRight { balance = it }
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return@launch
|
||||
}
|
||||
uiState.update(DetailsBalanceTransformer(balance = result))
|
||||
}.saveIn(fetchBalanceJobHolder)
|
||||
}
|
||||
|
|
@ -217,7 +235,12 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
|
||||
private fun fetchAddToWalletBanner() {
|
||||
modelScope.launch {
|
||||
val isDone = cardDetailsRepository.isAddToWalletDone().getOrNull() ?: false
|
||||
val isDone = try {
|
||||
cardDetailsRepository.isAddToWalletDone(params.userWalletId).getOrNull() == true
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return@launch
|
||||
}
|
||||
uiState.update(
|
||||
transformer = DetailsAddToWalletBannerTransformer(
|
||||
onClickBanner = ::onClickAddToWalletBlock,
|
||||
|
|
@ -244,7 +267,11 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
|
||||
private fun onClickCloseAddToWalletBlock() {
|
||||
modelScope.launch {
|
||||
cardDetailsRepository.setAddToWalletAsDone()
|
||||
try {
|
||||
cardDetailsRepository.setAddToWalletAsDone(params.userWalletId)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
uiState.update(
|
||||
transformer = DetailsAddToWalletBannerTransformer(
|
||||
onClickBanner = ::onClickAddToWalletBlock,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ internal class TangemPayTxHistoryModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun launchPagination() {
|
||||
modelScope.launch { listManager.launchPagination() }
|
||||
modelScope.launch { listManager.launchPagination(params.userWalletId) }
|
||||
}
|
||||
|
||||
private fun subscribeToUiItemChanges() {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@ internal class DetailsHiddenStateTransformer(
|
|||
) : Transformer<TangemPayCardDetailsUM> {
|
||||
|
||||
override fun transform(prevState: TangemPayCardDetailsUM): TangemPayCardDetailsUM {
|
||||
return stateFactory.getInitialState()
|
||||
return stateFactory.getInitialState().copy(cardFrozenState = prevState.cardFrozenState)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.tangempay.utils
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
|
||||
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListConfig
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
|
|
@ -36,8 +37,9 @@ internal class TangemPayTxHistoryListManager(
|
|||
val emptyStatus: Flow<Boolean> = state.map { it.isEmpty }.distinctUntilChanged()
|
||||
val paginationStatus: Flow<PaginationStatus<*>> = state.map { it.status }.distinctUntilChanged()
|
||||
|
||||
suspend fun launchPagination() = coroutineScope {
|
||||
suspend fun launchPagination(userWalletId: UserWalletId) = coroutineScope {
|
||||
val batchFlow = repository.getTxHistoryBatchFlow(
|
||||
userWalletId = userWalletId,
|
||||
context = TangemPayTxHistoryListBatchingContext(actionsFlow = actionsFlow, coroutineScope = this),
|
||||
batchSize = 50,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ dependencies {
|
|||
/** Domain */
|
||||
implementation(projects.domain.visa)
|
||||
|
||||
/** Data **/
|
||||
implementation(projects.data.visa)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ import com.tangem.core.decompose.di.ModelScoped
|
|||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.features.tangempay.TangemPayConstants
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.data.pay.util.TangemPayWalletsManager
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.features.tangempay.TangemPayConstants
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButtonLoadingTransformer
|
||||
import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState
|
||||
|
|
@ -17,13 +18,14 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import com.tangem.utils.transformer.update as transformerUpdate
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import com.tangem.utils.transformer.update as transformerUpdate
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class TangemPayOnboardingModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val router: Router,
|
||||
|
|
@ -31,6 +33,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
|||
private val repository: OnboardingRepository,
|
||||
private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val tangemPayWalletsManager: TangemPayWalletsManager,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TangemPayOnboardingComponent.Params>()
|
||||
|
|
@ -66,7 +69,11 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun checkCustomerInfo() {
|
||||
repository.getCustomerInfo()
|
||||
// TODO implement selector
|
||||
repository.getCustomerInfo(
|
||||
userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId,
|
||||
)
|
||||
// selector
|
||||
.onRight { customerInfo ->
|
||||
when {
|
||||
!customerInfo.isKycApproved -> {
|
||||
|
|
@ -88,32 +95,42 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
|||
private fun onGetCardClick() {
|
||||
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true))
|
||||
modelScope.launch {
|
||||
val result = produceInitialDataUseCase()
|
||||
// TODO implement selector
|
||||
val userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId
|
||||
val result = produceInitialDataUseCase(userWalletId)
|
||||
if (result.isLeft()) {
|
||||
Timber.e("Error producing initial data: ${result.leftOrNull()?.message}")
|
||||
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false))
|
||||
return@launch
|
||||
}
|
||||
|
||||
repository.getCustomerInfo()
|
||||
.fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error getCustomerInfo: ${it.errorCode}")
|
||||
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false))
|
||||
},
|
||||
ifRight = { customerInfo ->
|
||||
if (customerInfo.isKycApproved) {
|
||||
back()
|
||||
} else {
|
||||
openKyc()
|
||||
}
|
||||
},
|
||||
)
|
||||
// TODO implement selector
|
||||
repository.getCustomerInfo(
|
||||
userWalletId = userWalletId,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error getCustomerInfo: ${it.errorCode}")
|
||||
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false))
|
||||
},
|
||||
ifRight = { customerInfo ->
|
||||
if (customerInfo.isKycApproved) {
|
||||
back()
|
||||
} else {
|
||||
openKyc()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openKyc() {
|
||||
router.replaceAll(AppRoute.Wallet, AppRoute.Kyc)
|
||||
// TODO implement selector
|
||||
router.replaceAll(
|
||||
AppRoute.Wallet,
|
||||
AppRoute.Kyc(
|
||||
userWalletId = tangemPayWalletsManager.getDefaultWalletForTangemPayBlocking().walletId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun back() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.feature.wallet.child.wallet.model
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.MainScreenCustomerInfo
|
||||
import com.tangem.domain.pay.model.TangemPayCustomerInfoError
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class TangemPayMainInfoManager @Inject constructor(
|
||||
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
|
||||
) {
|
||||
|
||||
val mainScreenCustomerInfo:
|
||||
StateFlow<Pair<UserWalletId, Either<TangemPayCustomerInfoError, MainScreenCustomerInfo>>?>
|
||||
field = MutableStateFlow(null)
|
||||
|
||||
suspend fun refreshTangemPayInfo(userWalletId: UserWalletId) {
|
||||
val info = tangemPayMainScreenCustomerInfoUseCase(userWalletId)
|
||||
mainScreenCustomerInfo.value = Pair(userWalletId, info)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,10 +20,9 @@ import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase
|
|||
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.pay.model.MainScreenCustomerInfo
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.TangemPayCustomerInfoError
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.settings.*
|
||||
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
|
|
@ -94,13 +93,13 @@ internal class WalletModel @Inject constructor(
|
|||
private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase,
|
||||
private val tangemPayOnboardingRepository: OnboardingRepository,
|
||||
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val tangemPayMainInfoManager: TangemPayMainInfoManager,
|
||||
val screenLifecycleProvider: ScreenLifecycleProvider,
|
||||
val innerWalletRouter: InnerWalletRouter,
|
||||
) : Model() {
|
||||
|
|
@ -137,7 +136,8 @@ internal class WalletModel @Inject constructor(
|
|||
subscribeOnSelectedWalletFlow()
|
||||
subscribeToScreenBackgroundState()
|
||||
subscribeOnPushNotificationsPermission()
|
||||
subscribeToTangemPayInfo()
|
||||
subscribeTangemPayOnWalletState()
|
||||
subscribeOnTangemPayInfoUpdates()
|
||||
enableNotificationsIfNeeded()
|
||||
|
||||
clickIntents.initialize(innerWalletRouter, modelScope)
|
||||
|
|
@ -349,57 +349,86 @@ internal class WalletModel @Inject constructor(
|
|||
.saveIn(clearNFTCacheJobHolder)
|
||||
}
|
||||
|
||||
private fun subscribeToTangemPayInfo() {
|
||||
private fun subscribeTangemPayOnWalletState() {
|
||||
/**
|
||||
* Update state each time a user opens/returns to wallet screen
|
||||
* and every minute while user stays on the main screen
|
||||
*/
|
||||
screenLifecycleProvider.isBackgroundState.onEach { inBackground ->
|
||||
// fast exit
|
||||
if (!tangemPayFeatureToggles.isTangemPayEnabled) return@onEach
|
||||
if (!tangemPayFeatureToggles.isTangemPayEnabled) return
|
||||
|
||||
// Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh
|
||||
val savedCustomerInfo = tangemPayOnboardingRepository.getSavedCustomerInfo()
|
||||
if (savedCustomerInfo?.cardInfo != null) {
|
||||
updateTangemPay(MainScreenCustomerInfo(info = savedCustomerInfo, orderStatus = OrderStatus.COMPLETED))
|
||||
return@onEach
|
||||
}
|
||||
combine(
|
||||
flow = screenLifecycleProvider.isBackgroundState,
|
||||
flow2 = uiState.mapNotNull {
|
||||
it.wallets.getOrNull(it.selectedWalletIndex)?.walletCardState?.id
|
||||
}.distinctUntilChanged(),
|
||||
transform = ::Pair,
|
||||
).onEach { (inBackground, userWalletId) ->
|
||||
val savedCustomerInfo =
|
||||
tangemPayOnboardingRepository.getSavedCustomerInfo(userWalletId)
|
||||
|
||||
updateTangemPayJobHolder.cancel()
|
||||
val isShouldLaunchPeriodicUpdate = savedCustomerInfo?.cardInfo == null &&
|
||||
tangemPayOnboardingRepository.isTangemPayInitialDataProduced(userWalletId)
|
||||
|
||||
modelScope.launch {
|
||||
// fast exit
|
||||
val initialDataProduced = tangemPayOnboardingRepository.isTangemPayInitialDataProduced()
|
||||
if (!initialDataProduced) return@launch
|
||||
|
||||
if (!inBackground) {
|
||||
refreshTangemPayInfo()
|
||||
while (isActive) {
|
||||
delay(TANGEM_PAY_UPDATE_INTERVAL)
|
||||
refreshTangemPayInfo()
|
||||
if (isShouldLaunchPeriodicUpdate) {
|
||||
updateTangemPayJobHolder.cancel()
|
||||
modelScope.launch {
|
||||
if (!inBackground) {
|
||||
tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId)
|
||||
while (isActive) {
|
||||
delay(TANGEM_PAY_UPDATE_INTERVAL)
|
||||
tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.saveIn(updateTangemPayJobHolder)
|
||||
}.saveIn(updateTangemPayJobHolder)
|
||||
} else {
|
||||
// Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh
|
||||
tangemPayMainInfoManager.refreshTangemPayInfo(userWalletId)
|
||||
}
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun refreshTangemPayInfo() {
|
||||
val info = tangemPayMainScreenCustomerInfoUseCase()
|
||||
if (info != null) updateTangemPay(info)
|
||||
private fun subscribeOnTangemPayInfoUpdates() {
|
||||
if (!tangemPayFeatureToggles.isTangemPayEnabled) return
|
||||
tangemPayMainInfoManager.mainScreenCustomerInfo
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.onEach { (userWalletId, mainInfoData) ->
|
||||
mainInfoData.onLeft { tangemPayError ->
|
||||
when (tangemPayError) {
|
||||
TangemPayCustomerInfoError.RefreshNeededError -> {
|
||||
stateHolder.update(
|
||||
transformer = TangemPayRefreshNeededStateTransformer(
|
||||
onRefreshClick = { clickIntents.onRefreshPayToken(userWalletId) },
|
||||
),
|
||||
)
|
||||
}
|
||||
TangemPayCustomerInfoError.UnavailableError -> {
|
||||
stateHolder.update(
|
||||
transformer = TangemPayUnavailableStateTransformer,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// do not draw TangemPay block
|
||||
Timber.e("Failed when loading main screen TangemPay info: $tangemPayError")
|
||||
}
|
||||
}
|
||||
}.onRight { data -> updateTangemPay(data, userWalletId) }
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun updateTangemPay(info: MainScreenCustomerInfo) {
|
||||
private suspend fun updateTangemPay(data: MainScreenCustomerInfo, userWalletId: UserWalletId) {
|
||||
val cardFrozenState =
|
||||
info.info.productInstance?.cardId?.let { cardDetailsRepository.cardFrozenStateSync(it) }
|
||||
data.info.productInstance?.cardId?.let { cardDetailsRepository.cardFrozenStateSync(it) }
|
||||
?: TangemPayCardFrozenState.Unfrozen
|
||||
stateHolder.update(
|
||||
transformer = TangemPayInitialStateTransformer(
|
||||
value = info,
|
||||
value = data,
|
||||
cardFrozenState = cardFrozenState,
|
||||
onClickKyc = innerWalletRouter::openTangemPayOnboarding,
|
||||
openDetails = { config ->
|
||||
innerWalletRouter.openTangemPayDetails(
|
||||
userWalletId = stateHolder.getSelectedWalletId(),
|
||||
userWalletId = userWalletId,
|
||||
config = config,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,45 +1,45 @@
|
|||
package com.tangem.feature.wallet.child.wallet.model.intents
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.feature.wallet.child.wallet.model.TangemPayMainInfoManager
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayInitialStateTransformer
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface TangemPayIntents {
|
||||
|
||||
suspend fun onPullToRefresh()
|
||||
|
||||
fun onRefreshPayToken(userWalletId: UserWalletId)
|
||||
}
|
||||
|
||||
@ModelScoped
|
||||
internal class TangemPayClickIntentsImplementor @Inject constructor(
|
||||
private val stateHolder: WalletStateController,
|
||||
private val mainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
|
||||
private val featureToggles: TangemPayFeatureToggles,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val produceInitialDataTangemPay: ProduceTangemPayInitialDataUseCase,
|
||||
private val tangemPayInfoManager: TangemPayMainInfoManager,
|
||||
) : BaseWalletClickIntents(), TangemPayIntents {
|
||||
|
||||
override suspend fun onPullToRefresh() {
|
||||
if (!featureToggles.isTangemPayEnabled || !onboardingRepository.isTangemPayInitialDataProduced()) return
|
||||
val info = mainScreenCustomerInfoUseCase()
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
if (info != null) {
|
||||
val cardFrozenState =
|
||||
info.info.productInstance?.cardId?.let { cardDetailsRepository.cardFrozenStateSync(it) }
|
||||
?: TangemPayCardFrozenState.Unfrozen
|
||||
stateHolder.update(
|
||||
transformer = TangemPayInitialStateTransformer(
|
||||
value = info,
|
||||
onClickKyc = router::openTangemPayOnboarding,
|
||||
openDetails = { config -> router.openTangemPayDetails(userWalletId, config) },
|
||||
cardFrozenState = cardFrozenState,
|
||||
),
|
||||
)
|
||||
if (!featureToggles.isTangemPayEnabled ||
|
||||
!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)
|
||||
) {
|
||||
return
|
||||
}
|
||||
tangemPayInfoManager.refreshTangemPayInfo(userWalletId)
|
||||
}
|
||||
|
||||
override fun onRefreshPayToken(userWalletId: UserWalletId) {
|
||||
modelScope.launch {
|
||||
produceInitialDataTangemPay.invoke(userWalletId)
|
||||
tangemPayInfoManager.refreshTangemPayInfo(userWalletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent
|
||||
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program
|
||||
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
|
|
@ -50,16 +49,16 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
|||
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
|
||||
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
|
||||
is WalletNotification.NoteMigration -> MainScreen.NotePromo
|
||||
is WalletNotification.SwapPromo -> PromoAnalyticsEvent.NoticePromotionBanner(
|
||||
is WalletNotification.SwapPromo -> NoticePromotionBanner(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
program = Program.Empty, // Use it on new promo action
|
||||
)
|
||||
is WalletNotification.Sepa -> PromoAnalyticsEvent.NoticePromotionBanner(
|
||||
is WalletNotification.Sepa -> NoticePromotionBanner(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
program = Program.Sepa,
|
||||
)
|
||||
is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo
|
||||
is WalletNotification.VisaPresalePromo -> PromoAnalyticsEvent.VisaWaitlistPromo
|
||||
is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo
|
||||
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
|
||||
is WalletNotification.Informational.NoAccount,
|
||||
is WalletNotification.Warning.LowSignatures,
|
||||
|
|
@ -73,6 +72,8 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
|||
is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport
|
||||
is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond
|
||||
is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner
|
||||
is WalletNotification.Warning.TangemPayRefreshNeeded -> null
|
||||
WalletNotification.Warning.TangemPayUnreachable -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,4 +23,10 @@ internal sealed class TangemPayState {
|
|||
val balanceText: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
) : TangemPayState()
|
||||
|
||||
data class RefreshNeeded(
|
||||
val notification: WalletNotification,
|
||||
) : TangemPayState()
|
||||
|
||||
data class TemporaryUnavailable(val notification: WalletNotification) : TangemPayState()
|
||||
}
|
||||
|
|
@ -143,6 +143,24 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
title = resourceReference(R.string.yield_module_main_view_approve_notification_title),
|
||||
subtitle = resourceReference(R.string.yield_module_main_view_approve_notification_description),
|
||||
)
|
||||
|
||||
data object TangemPayUnreachable : Warning(
|
||||
title = resourceReference(id = R.string.tangempay_temporarily_unavailable),
|
||||
subtitle = resourceReference(id = R.string.tangempay_service_unreachable_try_later),
|
||||
)
|
||||
|
||||
data class TangemPayRefreshNeeded(
|
||||
@DrawableRes val tangemIcon: Int?,
|
||||
val onRefreshClick: () -> Unit,
|
||||
) : Warning(
|
||||
title = resourceReference(id = R.string.tangempay_payment_account_sync_needed),
|
||||
subtitle = resourceReference(id = R.string.tangempay_use_tangem_device_to_restore_payment_account),
|
||||
buttonsState = ButtonsState.PrimaryButtonConfig(
|
||||
text = resourceReference(id = R.string.home_button_scan),
|
||||
iconResId = tangemIcon,
|
||||
onClick = onRefreshClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Informational(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.transformers
|
||||
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
|
||||
|
||||
internal class TangemPayRefreshNeededStateTransformer(
|
||||
private val onRefreshClick: () -> Unit,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
val tangemPayState = TangemPayState.RefreshNeeded(
|
||||
notification = TangemPayRefreshNeeded(
|
||||
tangemIcon = R.drawable.ic_tangem_24,
|
||||
onRefreshClick = onRefreshClick,
|
||||
),
|
||||
)
|
||||
return prevState.copy(tangemPayState = tangemPayState)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.transformers
|
||||
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
|
||||
|
||||
internal object TangemPayUnavailableStateTransformer : WalletScreenStateTransformer {
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
tangemPayState = TangemPayState.TemporaryUnavailable(
|
||||
notification = WalletNotification.Warning.TangemPayUnreachable,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.unit.dp
|
|||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState.*
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -30,7 +31,7 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo
|
|||
iconSize = 36.dp,
|
||||
subtitle = state.description,
|
||||
iconResId = state.iconRes,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
buttonsState = SecondaryButtonConfig(
|
||||
text = state.buttonText,
|
||||
onClick = state.onButtonClick,
|
||||
shouldShowProgress = state.showProgress,
|
||||
|
|
@ -40,13 +41,15 @@ internal fun TangemPayMainScreenBlock(state: TangemPayState, isBalanceHidden: Bo
|
|||
}
|
||||
is TangemPayState.Card -> TangemPayCardMainBlock(state, isBalanceHidden, modifier)
|
||||
is TangemPayState.Empty -> Unit
|
||||
is TangemPayState.RefreshNeeded -> TangemPayRefreshBlock(state, modifier)
|
||||
is TangemPayState.TemporaryUnavailable -> TangemPayUnavailableBlock(state, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ResetCardScreenPreview() {
|
||||
private fun TangemPayMainScreenBlockPreview() {
|
||||
TangemThemePreview {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
|
||||
TangemPayMainScreenBlock(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components.visa
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.inputrow.InputRowImageBase
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded
|
||||
|
||||
@Composable
|
||||
internal fun TangemPayRefreshBlock(state: TangemPayState.RefreshNeeded, modifier: Modifier = Modifier) {
|
||||
Column(modifier) {
|
||||
Notification(
|
||||
config = state.notification.config,
|
||||
iconTint = when (state.notification) {
|
||||
is WalletNotification.Critical -> TangemTheme.colors.icon.warning
|
||||
is WalletNotification.Informational -> TangemTheme.colors.icon.accent
|
||||
is WalletNotification.RateApp -> TangemTheme.colors.icon.attention
|
||||
is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1
|
||||
is WalletNotification.UsedOutdatedData -> TangemTheme.colors.text.attention
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
SpacerH12()
|
||||
|
||||
BlockCard(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
) {
|
||||
InputRowImageBase(
|
||||
modifier = Modifier.padding(
|
||||
all = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
subtitle = resourceReference(R.string.tangempay_payment_account),
|
||||
caption = resourceReference(R.string.tangempay_payment_account_sync_needed),
|
||||
subtitleColor = TangemTheme.colors.text.tertiary,
|
||||
captionColor = TangemTheme.colors.text.tertiary,
|
||||
iconResWebp = R.drawable.img_visa_36,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TangemPayRefreshBlockPreview() {
|
||||
TangemThemePreview {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
|
||||
TangemPayRefreshBlock(
|
||||
state = TangemPayState.RefreshNeeded(
|
||||
TangemPayRefreshNeeded(
|
||||
tangemIcon = R.drawable.ic_tangem_24,
|
||||
onRefreshClick = {},
|
||||
),
|
||||
),
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components.visa
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.inputrow.InputRowImageBase
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.WalletNotification
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
|
||||
@Composable
|
||||
internal fun TangemPayUnavailableBlock(state: TangemPayState.TemporaryUnavailable, modifier: Modifier = Modifier) {
|
||||
Column(modifier) {
|
||||
Notification(
|
||||
config = state.notification.config,
|
||||
iconTint = when (state.notification) {
|
||||
is WalletNotification.Critical -> TangemTheme.colors.icon.warning
|
||||
is WalletNotification.Informational -> TangemTheme.colors.icon.accent
|
||||
is WalletNotification.RateApp -> TangemTheme.colors.icon.attention
|
||||
is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1
|
||||
is WalletNotification.UsedOutdatedData -> TangemTheme.colors.text.attention
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
SpacerH12()
|
||||
|
||||
BlockCard(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius14))
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
) {
|
||||
InputRowImageBase(
|
||||
modifier = Modifier.padding(
|
||||
all = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
subtitle = resourceReference(R.string.tangempay_payment_account),
|
||||
caption = TextReference.Str(DASH_SIGN),
|
||||
subtitleColor = TangemTheme.colors.text.tertiary,
|
||||
captionColor = TangemTheme.colors.text.tertiary,
|
||||
iconResWebp = R.drawable.img_visa_36,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TangemPayUnavailableBlockPreview() {
|
||||
TangemThemePreview {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
|
||||
TangemPayUnavailableBlock(
|
||||
state = TangemPayState.TemporaryUnavailable(
|
||||
WalletNotification.Warning.TangemPayUnreachable,
|
||||
),
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue