Updated on 2026-08-14
This commit is contained in:
parent
8aa7bf9b75
commit
4032360f13
34 changed files with 679 additions and 101 deletions
|
|
@ -14,7 +14,6 @@ import javax.inject.Singleton
|
|||
import kotlin.text.encodeToByteArray
|
||||
|
||||
private const val DEFAULT_KEY = "tangem_pay_default_key"
|
||||
private const val DEFAULT_CUSTOMER_WALLET_ADDRESS_KEY = "tangem_pay_default_customer_wallet_address_key"
|
||||
|
||||
@Singleton
|
||||
internal class DefaultTangemPayStorage @Inject constructor(
|
||||
|
|
@ -54,21 +53,6 @@ internal class DefaultTangemPayStorage @Inject constructor(
|
|||
?.let(tokensAdapter::fromJson)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the only customer wallet address, since for the f&f user can issue only one card tied to one address
|
||||
*/
|
||||
override suspend fun storeCustomerWalletAddress(customerWalletAddress: String) =
|
||||
withContext(dispatcherProvider.io) {
|
||||
secureStorage.store(
|
||||
customerWalletAddress.encodeToByteArray(throwOnInvalidSequence = true),
|
||||
DEFAULT_CUSTOMER_WALLET_ADDRESS_KEY,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCustomerWalletAddress(): String? = withContext(dispatcherProvider.io) {
|
||||
secureStorage.get(DEFAULT_CUSTOMER_WALLET_ADDRESS_KEY)?.decodeToString(throwOnInvalidSequence = true)
|
||||
}
|
||||
|
||||
override suspend fun clear(customerWalletAddress: String) = withContext(dispatcherProvider.io) {
|
||||
secureStorage.delete(createKey(customerWalletAddress))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import com.tangem.features.details.component.DetailsComponent
|
|||
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
||||
import com.tangem.features.home.api.HomeComponent
|
||||
import com.tangem.features.hotwallet.*
|
||||
import com.tangem.features.kyc.KycComponent
|
||||
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
|
|
@ -35,6 +36,7 @@ import com.tangem.features.swap.SwapComponent
|
|||
import com.tangem.features.swap.v2.api.SendWithSwapComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.*
|
||||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||
import com.tangem.features.wallet.WalletEntryComponent
|
||||
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
|
||||
|
|
@ -106,6 +108,7 @@ internal class ChildFactory @Inject constructor(
|
|||
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
|
||||
private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory,
|
||||
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
|
||||
private val kycComponentFactory: KycComponent.Factory,
|
||||
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) {
|
||||
|
|
@ -597,10 +600,20 @@ internal class ChildFactory @Inject constructor(
|
|||
is AppRoute.TangemPayOnboarding -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = TangemPayOnboardingComponent.Params(route.deeplink),
|
||||
params = when (val mode = route.mode) {
|
||||
is AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding -> ContinueOnboarding
|
||||
is AppRoute.TangemPayOnboarding.Mode.Deeplink -> Deeplink(deeplink = mode.deeplink)
|
||||
},
|
||||
componentFactory = tangemPayOnboardingComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.Kyc -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = KycComponent.Params,
|
||||
componentFactory = kycComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.YieldSupplyPromo -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
|
|
|
|||
|
|
@ -376,8 +376,23 @@ sealed class AppRoute(val path: String) : Route {
|
|||
|
||||
@Serializable
|
||||
data class TangemPayOnboarding(
|
||||
val deeplink: String,
|
||||
) : AppRoute(path = "/tangem_pay_onboarding/$deeplink")
|
||||
val mode: Mode,
|
||||
) : AppRoute(path = "/tangem_pay_onboarding/$mode") {
|
||||
|
||||
@Serializable
|
||||
sealed class Mode {
|
||||
@Serializable
|
||||
data class Deeplink(
|
||||
val deeplink: String,
|
||||
) : Mode()
|
||||
|
||||
@Serializable
|
||||
object ContinueOnboarding : Mode()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object Kyc : AppRoute(path = "/kyc")
|
||||
|
||||
@Serializable
|
||||
data class YieldSupplyPromo(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.pay.models.response
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerMeResponse(
|
||||
|
|
@ -16,6 +17,8 @@ data class CustomerMeResponse(
|
|||
@Json(name = "product_instance") val productInstance: ProductInstance?,
|
||||
@Json(name = "payment_account") val paymentAccount: PaymentAccount?,
|
||||
@Json(name = "kyc") val kyc: Kyc?,
|
||||
@Json(name = "card") val card: Card?,
|
||||
@Json(name = "balance") val balance: Balance?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
@ -45,4 +48,25 @@ data class CustomerMeResponse(
|
|||
@Json(name = "review_answer") val reviewAnswer: String,
|
||||
@Json(name = "created_at") val createdAt: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Card(
|
||||
@Json(name = "token") val token: String,
|
||||
@Json(name = "expiration_month") val expirationMonth: Int,
|
||||
@Json(name = "expiration_year") val expirationYear: Int,
|
||||
@Json(name = "emboss_name") val embossName: String,
|
||||
@Json(name = "card_type") val cardType: String,
|
||||
@Json(name = "card_status") val cardStatus: String,
|
||||
@Json(name = "card_number_end") val cardNumberEnd: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Balance(
|
||||
@Json(name = "currency") val currency: String,
|
||||
@Json(name = "available_balance") val availableBalance: BigDecimal,
|
||||
@Json(name = "credit_limit") val creditLimit: BigDecimal,
|
||||
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
|
||||
@Json(name = "posted_charges") val postedCharges: BigDecimal,
|
||||
@Json(name = "balance_due") val balanceDue: BigDecimal,
|
||||
)
|
||||
}
|
||||
|
|
@ -8,9 +8,5 @@ interface TangemPayStorage {
|
|||
|
||||
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
|
||||
|
||||
suspend fun storeCustomerWalletAddress(customerWalletAddress: String)
|
||||
|
||||
suspend fun getCustomerWalletAddress(): String?
|
||||
|
||||
suspend fun clear(customerWalletAddress: String)
|
||||
}
|
||||
23
core/ui/src/main/res/drawable/ic_promo_kyc_36.xml
Normal file
23
core/ui/src/main/res/drawable/ic_promo_kyc_36.xml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="36">
|
||||
<path
|
||||
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
|
||||
android:strokeAlpha="0.1"
|
||||
android:fillColor="#0099FF"
|
||||
android:fillAlpha="0.1"/>
|
||||
<path
|
||||
android:pathData="M17.758,21.123C18.172,21.12 18.51,21.453 18.514,21.867C18.517,22.281 18.184,22.619 17.771,22.623C16.646,22.633 15.542,23.074 14.779,23.887C14.496,24.189 14.021,24.203 13.719,23.92C13.417,23.636 13.402,23.161 13.686,22.859C14.756,21.719 16.265,21.136 17.758,21.123ZM18.982,17.373C18.982,16.671 18.425,16.123 17.764,16.123C17.102,16.123 16.546,16.672 16.546,17.373C16.546,18.075 17.102,18.623 17.764,18.623C18.425,18.623 18.982,18.075 18.982,17.373ZM20.482,17.373C20.482,18.881 19.276,20.123 17.764,20.123C16.251,20.123 15.046,18.881 15.046,17.373C15.046,15.866 16.251,14.623 17.764,14.623C19.276,14.623 20.482,15.866 20.482,17.373Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
<path
|
||||
android:pathData="M8.482,20.742V16.006C8.482,14.465 8.481,13.23 8.618,12.262C8.759,11.268 9.06,10.447 9.742,9.801C10.346,9.229 11.088,8.946 11.974,8.799C12.841,8.655 13.915,8.63 15.229,8.625C15.644,8.624 15.981,8.958 15.982,9.372C15.984,9.786 15.649,10.123 15.235,10.125C13.905,10.13 12.95,10.157 12.22,10.278C11.508,10.397 11.086,10.594 10.773,10.89C10.431,11.215 10.219,11.661 10.104,12.473C9.985,13.311 9.982,14.421 9.982,16.006V20.742C9.982,22.328 9.985,23.437 10.104,24.275C10.219,25.087 10.431,25.534 10.773,25.858C11.122,26.189 11.61,26.397 12.486,26.509C13.381,26.623 14.562,26.624 16.232,26.624H18.232C18.647,26.624 18.982,26.96 18.982,27.374C18.982,27.788 18.647,28.124 18.232,28.124H16.232C14.603,28.124 13.308,28.125 12.297,27.996C11.268,27.865 10.418,27.588 9.742,26.947C9.06,26.301 8.759,25.48 8.618,24.486C8.481,23.519 8.482,22.283 8.482,20.742ZM25.482,16.874V16.006C25.482,14.421 25.48,13.311 25.361,12.473C25.246,11.661 25.034,11.215 24.691,10.89C24.379,10.594 23.956,10.397 23.245,10.278C22.515,10.157 21.56,10.13 20.229,10.125C19.815,10.123 19.481,9.786 19.482,9.372C19.484,8.958 19.821,8.624 20.235,8.625C21.549,8.63 22.624,8.655 23.491,8.799C24.377,8.946 25.119,9.229 25.723,9.801C26.405,10.447 26.706,11.268 26.847,12.262C26.984,13.23 26.982,14.465 26.982,16.006V16.874C26.982,17.288 26.647,17.624 26.232,17.624C25.818,17.624 25.483,17.288 25.482,16.874Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
<path
|
||||
android:pathData="M17.733,6.623C18.164,6.623 18.503,6.618 18.81,6.689C19.473,6.841 20.057,7.255 20.383,7.855C20.536,8.136 20.606,8.466 20.691,8.836L20.774,9.195C20.856,9.547 20.931,9.869 20.964,10.138C20.998,10.417 21,10.74 20.854,11.062C20.704,11.391 20.455,11.659 20.153,11.843C19.867,12.016 19.558,12.073 19.27,12.099C18.986,12.124 18.634,12.123 18.231,12.123H17.235C16.831,12.123 16.48,12.124 16.195,12.099C15.907,12.073 15.598,12.016 15.313,11.843C15.01,11.659 14.761,11.391 14.611,11.062C14.465,10.74 14.467,10.417 14.501,10.138C14.534,9.869 14.609,9.547 14.691,9.195L14.774,8.836C14.859,8.466 14.929,8.136 15.082,7.855C15.408,7.255 15.992,6.841 16.655,6.689C16.962,6.618 17.301,6.623 17.733,6.623ZM17.733,8.123C17.223,8.123 17.092,8.128 16.992,8.15C16.71,8.215 16.503,8.382 16.399,8.571C16.368,8.629 16.342,8.714 16.236,9.174L16.153,9.534C16.064,9.918 16.011,10.148 15.99,10.318C15.981,10.391 15.982,10.432 15.983,10.45C16.002,10.486 16.035,10.527 16.091,10.562C16.092,10.562 16.095,10.563 16.098,10.564C16.104,10.567 16.115,10.571 16.133,10.575C16.17,10.585 16.23,10.596 16.326,10.605C16.53,10.622 16.804,10.623 17.235,10.623H18.231C18.661,10.623 18.935,10.622 19.139,10.605C19.235,10.596 19.295,10.585 19.332,10.575C19.35,10.571 19.361,10.567 19.367,10.564C19.37,10.563 19.372,10.562 19.373,10.562C19.429,10.528 19.462,10.486 19.481,10.45C19.482,10.432 19.484,10.392 19.475,10.318C19.454,10.148 19.401,9.918 19.313,9.534L19.23,9.174C19.123,8.714 19.097,8.629 19.066,8.571C18.962,8.382 18.755,8.215 18.473,8.15C18.373,8.128 18.242,8.123 17.733,8.123Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
<path
|
||||
android:pathData="M27.483,23.373C27.483,21.578 26.028,20.123 24.233,20.123C22.438,20.123 20.983,21.578 20.983,23.373C20.983,25.168 22.438,26.623 24.233,26.623C26.028,26.623 27.483,25.168 27.483,23.373ZM22.572,24.493H22.574L22.575,24.494C22.202,24.316 22.045,23.868 22.223,23.494C22.401,23.121 22.848,22.963 23.222,23.141L23.224,23.142L23.227,23.143C23.228,23.143 23.23,23.145 23.232,23.146C23.235,23.147 23.24,23.149 23.244,23.151C23.253,23.156 23.265,23.162 23.278,23.169C23.303,23.183 23.337,23.202 23.376,23.226C23.451,23.272 23.549,23.338 23.658,23.426C23.669,23.409 23.68,23.393 23.691,23.376C24.021,22.875 24.552,22.178 25.21,21.824C25.575,21.628 26.03,21.764 26.227,22.129C26.423,22.494 26.286,22.948 25.921,23.145C25.618,23.308 25.26,23.722 24.942,24.203C24.795,24.427 24.673,24.637 24.589,24.791C24.547,24.868 24.515,24.93 24.493,24.972C24.483,24.993 24.475,25.009 24.47,25.019C24.467,25.024 24.466,25.027 24.465,25.029C24.345,25.279 24.097,25.444 23.82,25.456C23.544,25.468 23.282,25.326 23.142,25.087C23.005,24.855 22.847,24.698 22.729,24.603C22.67,24.555 22.622,24.523 22.593,24.505L22.572,24.493ZM28.983,23.373C28.983,25.997 26.856,28.123 24.233,28.123C21.609,28.123 19.483,25.997 19.483,23.373C19.483,20.75 21.609,18.623 24.233,18.623C26.856,18.623 28.983,20.75 28.983,23.373Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="36">
|
||||
<path
|
||||
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
|
||||
android:strokeAlpha="0.1"
|
||||
android:fillColor="#0099FF"
|
||||
android:fillAlpha="0.1"/>
|
||||
<path
|
||||
android:pathData="M9,10L27,10A3,3 0,0 1,30 13L30,23A3,3 0,0 1,27 26L9,26A3,3 0,0 1,6 23L6,13A3,3 0,0 1,9 10z"
|
||||
android:fillColor="#0099FF"/>
|
||||
<path
|
||||
android:pathData="M13.051,20.582H11.502L10.34,16.5C10.285,16.312 10.167,16.146 9.995,16.068C9.565,15.871 9.092,15.715 8.575,15.636V15.479H11.071C11.415,15.479 11.674,15.715 11.717,15.989L12.32,18.933L13.868,15.479H15.375L13.051,20.582ZM16.235,20.58H14.771L15.976,15.477H17.44L16.235,20.58ZM19.335,16.891C19.378,16.616 19.636,16.459 19.938,16.459C20.412,16.419 20.927,16.498 21.358,16.694L21.617,15.595C21.186,15.438 20.712,15.359 20.282,15.359C18.862,15.359 17.829,16.066 17.829,17.047C17.829,17.794 18.561,18.185 19.077,18.422C19.636,18.657 19.852,18.814 19.809,19.049C19.809,19.403 19.378,19.56 18.948,19.56C18.432,19.56 17.915,19.442 17.442,19.246L17.184,20.345C17.7,20.541 18.259,20.62 18.776,20.62C20.368,20.659 21.358,19.952 21.358,18.892C21.358,17.558 19.335,17.479 19.335,16.891ZM26.48,20.58L25.318,15.477H24.07C23.811,15.477 23.553,15.634 23.467,15.869L21.315,20.58H22.822L23.122,19.834H24.973L25.146,20.58H26.48ZM24.283,16.852L24.713,18.775H23.508L24.283,16.852Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="36">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"/>
|
||||
<path
|
||||
android:pathData="M0,31.5l0,-31.5l36,-0l0,31.5z"
|
||||
android:fillColor="#F5F7F8"/>
|
||||
<path
|
||||
android:pathData="M18,9l0,-4.5l18,-0l0,4.5z"
|
||||
android:fillColor="#DD2033"/>
|
||||
<path
|
||||
android:pathData="M18,18l0,-4.5l18,-0l0,4.5z"
|
||||
android:fillColor="#DD2033"/>
|
||||
<path
|
||||
android:pathData="M0,27l0,-4.5l36,-0l0,4.5z"
|
||||
android:fillColor="#DD2033"/>
|
||||
<path
|
||||
android:pathData="M0,36l0,-4.5l36,-0l0,4.5z"
|
||||
android:fillColor="#DD2033"/>
|
||||
<path
|
||||
android:pathData="M0,18l0,-18l18,-0l0,18z"
|
||||
android:fillColor="#004692"/>
|
||||
<path
|
||||
android:pathData="M14.2,11L13.306,13.023L11.2,13.292L12.753,14.811L12.346,17L14.2,15.75L16.054,17L15.647,14.811L17.2,13.292L15.094,13.023L14.2,11Z"
|
||||
android:fillColor="#F5F7F8"/>
|
||||
<path
|
||||
android:pathData="M14.2,-1L13.306,1.023L11.2,1.292L12.753,2.811L12.346,5L14.2,3.75L16.054,5L15.647,2.811L17.2,1.292L15.094,1.023L14.2,-1Z"
|
||||
android:fillColor="#F5F7F8"/>
|
||||
<path
|
||||
android:pathData="M10.2,5L9.306,7.023L7.2,7.292L8.753,8.811L8.346,11L10.2,9.75L12.054,11L11.647,8.811L13.2,7.292L11.094,7.023L10.2,5Z"
|
||||
android:fillColor="#F5F7F8"/>
|
||||
<path
|
||||
android:pathData="M5.7,11L4.806,13.023L2.7,13.292L4.253,14.811L3.846,17L5.7,15.75L7.554,17L7.147,14.811L8.7,13.292L6.594,13.023L5.7,11Z"
|
||||
android:fillColor="#F5F7F8"/>
|
||||
<path
|
||||
android:pathData="M5.7,-1L4.806,1.023L2.7,1.292L4.253,2.811L3.846,5L5.7,3.75L7.554,5L7.147,2.811L8.7,1.292L6.594,1.023L5.7,-1Z"
|
||||
android:fillColor="#F5F7F8"/>
|
||||
<path
|
||||
android:pathData="M1.7,5L0.806,7.023L-1.3,7.292L0.253,8.811L-0.154,11L1.7,9.75L3.554,11L3.147,8.811L4.7,7.292L2.594,7.023L1.7,5Z"
|
||||
android:fillColor="#F5F7F8"/>
|
||||
</group>
|
||||
</vector>
|
||||
17
core/ui/src/main/res/drawable/img_visa_label_26_16.xml
Normal file
17
core/ui/src/main/res/drawable/img_visa_label_26_16.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="26dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="26"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M3,0L23,0A3,3 0,0 1,26 3L26,13A3,3 0,0 1,23 16L3,16A3,3 0,0 1,0 13L0,3A3,3 0,0 1,3 0z"
|
||||
android:strokeAlpha="0.5"
|
||||
android:fillColor="#474E71"
|
||||
android:fillAlpha="0.4"/>
|
||||
<path
|
||||
android:pathData="M7.521,11.139H5.788L4.488,6.193C4.427,5.966 4.296,5.765 4.103,5.67C3.622,5.431 3.093,5.242 2.515,5.146V4.956H5.306C5.692,4.956 5.981,5.242 6.029,5.574L6.703,9.142L8.435,4.956H10.12L7.521,11.139ZM11.082,11.139H9.445L10.793,4.956H12.429L11.082,11.139ZM14.549,6.668C14.597,6.335 14.886,6.145 15.223,6.145C15.753,6.097 16.33,6.192 16.811,6.43L17.1,5.099C16.619,4.908 16.089,4.813 15.608,4.813C14.02,4.813 12.864,5.669 12.864,6.858C12.864,7.762 13.683,8.237 14.261,8.523C14.886,8.808 15.127,8.998 15.078,9.284C15.078,9.712 14.597,9.902 14.116,9.902C13.538,9.902 12.96,9.759 12.431,9.521L12.142,10.853C12.72,11.091 13.345,11.186 13.924,11.186C15.705,11.233 16.811,10.378 16.811,9.093C16.811,7.476 14.549,7.381 14.549,6.668ZM22.54,11.139L21.241,4.956H19.845C19.556,4.956 19.267,5.146 19.171,5.431L16.764,11.139H18.449L18.785,10.236H20.855L21.048,11.139H22.54ZM20.083,6.621L20.564,8.951H19.216L20.083,6.621Z"
|
||||
android:strokeAlpha="0.5"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"
|
||||
android:fillAlpha="0.5"/>
|
||||
</vector>
|
||||
|
|
@ -5,9 +5,11 @@ import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
|
|||
import com.tangem.data.pay.repository.DefaultOnboardingRepository
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -27,4 +29,14 @@ internal interface TangemPayDataModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayTxHistoryRepository(repository: DefaultTangemPayTxHistoryRepository): TangemPayTxHistoryRepository
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayMainScreenCustomerInfoUseCase(
|
||||
repository: OnboardingRepository,
|
||||
): TangemPayMainScreenCustomerInfoUseCase {
|
||||
return TangemPayMainScreenCustomerInfoUseCase(repository = repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,10 @@ import arrow.core.raise.either
|
|||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
|
||||
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
||||
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.ProductInstance
|
||||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -26,11 +28,31 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
override suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo> = either {
|
||||
return requestHelper.request { authHeader ->
|
||||
tangemPayApi.getCustomerMe(authHeader)
|
||||
}.map {
|
||||
CustomerInfo(
|
||||
productInstance = it.result?.productInstance?.let { ProductInstance(id = it.id, status = it.status) },
|
||||
kycStatus = it.result?.kyc?.status,
|
||||
}.map { getCustomerInfo(it.result) }
|
||||
}
|
||||
|
||||
override suspend fun getMainScreenCustomerInfo(): Either<UniversalError, CustomerInfo> = either {
|
||||
return requestHelper.requestWithPersistedToken { authHeader ->
|
||||
tangemPayApi.getCustomerMe(authHeader)
|
||||
}.map { getCustomerInfo(it.result) }
|
||||
}
|
||||
|
||||
private fun getCustomerInfo(response: CustomerMeResponse.Result?): CustomerInfo {
|
||||
val card = response?.card
|
||||
val balance = response?.balance
|
||||
val cardInfo = if (card != null && balance != null) {
|
||||
CardInfo(
|
||||
lastFourDigits = card.cardNumberEnd,
|
||||
balance = balance.availableBalance,
|
||||
currencyCode = balance.currency,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return CustomerInfo(
|
||||
productInstance = response?.productInstance?.let { ProductInstance(id = it.id, status = it.status) },
|
||||
kycStatus = response?.kyc?.status,
|
||||
cardInfo = cardInfo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,18 +10,20 @@ import com.tangem.datasource.api.common.response.getOrThrow
|
|||
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -35,9 +37,9 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tangemPayStorage: TangemPayStorage,
|
||||
private val userWalletsRepository: UserWalletsListRepository,
|
||||
private val getCurrencyUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
private val authDataSource: TangemPayAuthDataSource,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
) {
|
||||
private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi)
|
||||
|
||||
|
|
@ -49,22 +51,39 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
suspend fun <T : Any> request(requestBlock: suspend (header: String) -> ApiResponse<T>): Either<UniversalError, T> =
|
||||
either {
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(requestBlock = requestBlock, refreshTokens = ::refreshAuthTokens).bind()
|
||||
performRequest(
|
||||
requestBlock = requestBlock,
|
||||
getTokens = ::getAccessTokens,
|
||||
refreshTokens = ::refreshAuthTokens,
|
||||
).bind()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T : Any> requestWithPersistedToken(
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
): Either<UniversalError, T> = either {
|
||||
withContext(dispatchers.io) {
|
||||
performRequest(
|
||||
requestBlock = requestBlock,
|
||||
getTokens = ::getAccessTokensIfSaved,
|
||||
refreshTokens = ::refreshAuthTokens,
|
||||
).bind()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T : Any> performRequest(
|
||||
requestBlock: suspend (header: String) -> ApiResponse<T>,
|
||||
getTokens: (suspend () -> Either<UniversalError, VisaAuthTokens>),
|
||||
refreshTokens: (suspend () -> Either<UniversalError, VisaAuthTokens>)? = null,
|
||||
): Either<UniversalError, T> = either {
|
||||
runCatching {
|
||||
requestBlock("Bearer ${getAccessTokens().bind().accessToken}").getOrThrow()
|
||||
requestBlock("Bearer ${getTokens().bind().accessToken}").getOrThrow()
|
||||
}.getOrElse { error ->
|
||||
when (error) {
|
||||
is ApiResponseError.HttpException -> {
|
||||
if (refreshTokens != null && error.code == ApiResponseError.HttpException.Code.UNAUTHORIZED) {
|
||||
refreshOrJoin(refreshTokens).bind()
|
||||
performRequest(requestBlock, refreshTokens = null).bind()
|
||||
performRequest(requestBlock, refreshTokens = null, getTokens = getTokens).bind()
|
||||
} else {
|
||||
raise(mapHttpError(error))
|
||||
}
|
||||
|
|
@ -103,9 +122,7 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun getCustomerWalletAddress(): Either<UniversalError, String> = either {
|
||||
customerWalletAddress
|
||||
?: tangemPayStorage.getCustomerWalletAddress()
|
||||
?: fetchAuthInputData().bind().address
|
||||
customerWalletAddress ?: fetchAuthInputData().bind().address
|
||||
}
|
||||
|
||||
private suspend fun getAccessTokens(): Either<UniversalError, VisaAuthTokens> = either {
|
||||
|
|
@ -113,6 +130,11 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
tangemPayStorage.getAuthTokens(address) ?: fetchTokens().bind()
|
||||
}
|
||||
|
||||
private suspend fun getAccessTokensIfSaved(): Either<UniversalError, VisaAuthTokens> = either {
|
||||
tangemPayStorage.getAuthTokens(getCustomerWalletAddress().bind())
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
}
|
||||
|
||||
private fun mapHttpError(throwable: ApiResponseError.HttpException): UniversalError {
|
||||
val errorBody = throwable.errorBody ?: return VisaApiError.UnknownWithoutCode
|
||||
return runCatching {
|
||||
|
|
@ -125,14 +147,16 @@ internal class TangemPayRequestPerformer @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun fetchAuthInputData(): Either<UniversalError, AuthInputData> = either {
|
||||
val wallet = userWalletsRepository.userWalletsSync().find { it is UserWallet.Cold } as? UserWallet.Cold
|
||||
val userWallets = getWalletsUseCase()
|
||||
.filter { it.isNotEmpty() }
|
||||
.first()
|
||||
val wallet = userWallets.find { it is UserWallet.Cold } as? UserWallet.Cold
|
||||
?: raise(VisaApiError.UnknownWithoutCode)
|
||||
|
||||
val address = getCurrencyUseCase.invokeMultiWalletSync(wallet.walletId, CryptoCurrency.ID.fromValue(POL_VALUE))
|
||||
.getOrNull()?.value?.networkAddress?.defaultAddress?.value ?: raise(VisaApiError.UnknownWithoutCode)
|
||||
|
||||
customerWalletAddress = address
|
||||
tangemPayStorage.storeCustomerWalletAddress(address)
|
||||
|
||||
AuthInputData(address, wallet.cardId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,28 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
private const val APPROVED_KYC_STATUS = "APPROVED"
|
||||
private const val ACTIVE_PI_STATUS = "active"
|
||||
|
||||
data class CustomerInfo(
|
||||
val productInstance: ProductInstance?,
|
||||
val kycStatus: String?,
|
||||
val cardInfo: CardInfo?,
|
||||
) {
|
||||
|
||||
data class ProductInstance(
|
||||
val id: String,
|
||||
val status: String,
|
||||
)
|
||||
|
||||
data class CardInfo(
|
||||
val lastFourDigits: String,
|
||||
val balance: BigDecimal,
|
||||
val currencyCode: String,
|
||||
)
|
||||
|
||||
fun isKycApproved() = kycStatus == APPROVED_KYC_STATUS
|
||||
|
||||
fun isProductInstanceActive() = productInstance?.status == ACTIVE_PI_STATUS
|
||||
}
|
||||
|
||||
data class ProductInstance(
|
||||
val id: String,
|
||||
val status: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -9,4 +9,9 @@ interface OnboardingRepository {
|
|||
suspend fun validateDeeplink(link: String): Either<UniversalError, Boolean>
|
||||
|
||||
suspend fun getCustomerInfo(): Either<UniversalError, CustomerInfo>
|
||||
|
||||
/**
|
||||
* Returns only if the user already authorised at least once
|
||||
*/
|
||||
suspend fun getMainScreenCustomerInfo(): Either<UniversalError, CustomerInfo>
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
|
||||
/**
|
||||
* Returns tangem pay customer info for the main screen banner
|
||||
* Works only if the user already authorised at least once (won't emit anything otherwise)
|
||||
*/
|
||||
class TangemPayMainScreenCustomerInfoUseCase(
|
||||
private val repository: OnboardingRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): CustomerInfo? = repository.getMainScreenCustomerInfo().getOrNull()
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
package com.tangem.features.kyc
|
||||
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface KycComponent {
|
||||
interface KycComponent : ComposableContentComponent {
|
||||
|
||||
fun launch()
|
||||
data object Params
|
||||
|
||||
interface Factory {
|
||||
fun create(appComponentContext: AppComponentContext): KycComponent
|
||||
}
|
||||
interface Factory : ComponentFactory<Params, KycComponent>
|
||||
}
|
||||
|
|
@ -1,45 +1,62 @@
|
|||
package com.tangem.features.kyc
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.sumsub.sns.core.SNSMobileSDK
|
||||
import com.sumsub.sns.core.data.listener.TokenExpirationHandler
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.domain.pay.KycStartInfo
|
||||
import com.tangem.features.kyc.theme.TangemSNSIconHandler
|
||||
import com.tangem.features.kyc.theme.TangemSNSTheme
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
class DefaultKycComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: KycComponent.Params,
|
||||
) : KycComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: DefaultKycModel = getOrCreateModel()
|
||||
private val model: DefaultKycModel = getOrCreateModel(params)
|
||||
|
||||
override fun launch() {
|
||||
init {
|
||||
componentScope.launch {
|
||||
model.uiState.collect {
|
||||
it?.let { startInfo ->
|
||||
val tokenExpirationHandler = object : TokenExpirationHandler {
|
||||
override fun onTokenExpired() = ""
|
||||
}
|
||||
val snsSdk = SNSMobileSDK.Builder(activity)
|
||||
.withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler)
|
||||
.withTheme(TangemSNSTheme.theme(activity))
|
||||
.withIconHandler(TangemSNSIconHandler())
|
||||
.withLocale(Locale(startInfo.locale))
|
||||
.build()
|
||||
snsSdk.launch()
|
||||
}
|
||||
model.uiState.drop(1).collectLatest { startInfo ->
|
||||
startInfo?.let { launchSdk(startInfo) }
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
model.getKycToken()
|
||||
}
|
||||
|
||||
private fun launchSdk(startInfo: KycStartInfo) {
|
||||
val tokenExpirationHandler = object : TokenExpirationHandler {
|
||||
/**
|
||||
* We don't refresh this token for f&f release. Assume it lives long enough to finish KYC
|
||||
* [REDACTED_TODO_COMMENT]
|
||||
*/
|
||||
override fun onTokenExpired() = ""
|
||||
}
|
||||
val snsSdk = SNSMobileSDK.Builder(activity)
|
||||
.withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler)
|
||||
.withTheme(TangemSNSTheme.theme(activity))
|
||||
.withIconHandler(TangemSNSIconHandler())
|
||||
.withLocale(Locale(startInfo.locale))
|
||||
.build()
|
||||
snsSdk.launch()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
KycLoadingScreen(router::pop, modifier)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : KycComponent.Factory {
|
||||
override fun create(appComponentContext: AppComponentContext): DefaultKycComponent
|
||||
override fun create(context: AppComponentContext, params: KycComponent.Params): DefaultKycComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ class DefaultKycModel @Inject constructor(
|
|||
private val _uiState: MutableStateFlow<KycStartInfo?> = MutableStateFlow(null)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
fun getKycToken() {
|
||||
init {
|
||||
modelScope.launch {
|
||||
kycRepository.getKycStartInfo().getOrNull()?.let { _uiState.emit(it) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.kyc
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun KycLoadingScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
onBackClick = onBack,
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier,
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ dependencies {
|
|||
implementation(projects.features.kyc.api)
|
||||
|
||||
implementation(projects.core.decompose)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
package com.tangem.features.kyc
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
/**
|
||||
* Mocking it for release/external builds to exclude SumSub dependency
|
||||
* This will never be called if the FT [isTangemPayEnabled] is off
|
||||
*/
|
||||
internal class MockKycComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
) : KycComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
override fun launch() {
|
||||
/* no op */
|
||||
}
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) { /* no op */ }
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : KycComponent.Factory {
|
||||
override fun create(appComponentContext: AppComponentContext): MockKycComponent
|
||||
override fun create(context: AppComponentContext, params: KycComponent.Params): MockKycComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -5,9 +5,13 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
|
|||
|
||||
interface TangemPayOnboardingComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val deeplink: String,
|
||||
)
|
||||
sealed class Params {
|
||||
data class Deeplink(
|
||||
val deeplink: String,
|
||||
) : Params()
|
||||
|
||||
object ContinueOnboarding : Params()
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, TangemPayOnboardingComponent>
|
||||
}
|
||||
|
|
@ -5,33 +5,29 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.features.kyc.KycComponent
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.tangempay.model.TangemPayOnboardingModel
|
||||
import com.tangem.features.tangempay.ui.TandemPayOnboardingScreen
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import com.tangem.features.tangempay.ui.TandemPayOnboardingScreen
|
||||
|
||||
internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: TangemPayOnboardingComponent.Params,
|
||||
kycComponentFactory: KycComponent.Factory,
|
||||
) : TangemPayOnboardingComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val kycComponent = kycComponentFactory.create(child("kycComponent"))
|
||||
|
||||
private val model: TangemPayOnboardingModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.screenState.collectAsStateWithLifecycle()
|
||||
TandemPayOnboardingScreen(modifier = modifier, state = state, onButtonClick = ::onButtonClick)
|
||||
}
|
||||
|
||||
private fun onButtonClick() {
|
||||
kycComponent.launch()
|
||||
TandemPayOnboardingScreen(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onBackClick = model::back,
|
||||
onOpenKycClick = model::openKyc,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ internal class DefaultOnboardVisaDeepLinkHandler @AssistedInject constructor(
|
|||
|
||||
init {
|
||||
if (tangemPayFeatureToggles.isTangemPayEnabled) {
|
||||
appRouter.push(AppRoute.TangemPayOnboarding(uri.toString()))
|
||||
val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink(uri.toString())
|
||||
appRouter.push(AppRoute.TangemPayOnboarding(mode))
|
||||
} else {
|
||||
appRouter.push(AppRoute.Home())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -30,29 +31,44 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
|||
|
||||
init {
|
||||
modelScope.launch {
|
||||
repository.validateDeeplink(params.deeplink)
|
||||
.onRight { isValid -> if (isValid) checkCustomerInfo() }
|
||||
.onLeft { exit() }
|
||||
when (params) {
|
||||
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> {
|
||||
checkCustomerInfo()
|
||||
}
|
||||
is TangemPayOnboardingComponent.Params.Deeplink -> {
|
||||
repository.validateDeeplink(params.deeplink)
|
||||
.onRight { isValid -> if (isValid) checkCustomerInfo() }
|
||||
.onLeft { back() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openKyc() {
|
||||
router.replaceAll(AppRoute.Wallet, AppRoute.Kyc)
|
||||
}
|
||||
|
||||
fun back() {
|
||||
router.pop()
|
||||
}
|
||||
|
||||
private suspend fun checkCustomerInfo() {
|
||||
repository.getCustomerInfo()
|
||||
.onRight { customerInfo ->
|
||||
when {
|
||||
!customerInfo.isKycApproved() -> {
|
||||
screenState.value = screenState.value.copy(fullScreenLoading = false)
|
||||
when (params) {
|
||||
is TangemPayOnboardingComponent.Params.Deeplink ->
|
||||
screenState.value = screenState.value.copy(fullScreenLoading = false)
|
||||
else -> openKyc()
|
||||
}
|
||||
}
|
||||
!customerInfo.isProductInstanceActive() -> {
|
||||
// TODO [REDACTED_TASK_KEY]: create order and poll order status (API is not ready yet)
|
||||
}
|
||||
else -> exit()
|
||||
else -> back()
|
||||
}
|
||||
}
|
||||
.onLeft { exit() }
|
||||
}
|
||||
|
||||
private fun exit() {
|
||||
router.pop()
|
||||
.onLeft { back() }
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,8 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
@Composable
|
||||
internal fun TandemPayOnboardingScreen(
|
||||
state: TangemPayOnboardingScreenState,
|
||||
onButtonClick: () -> Unit,
|
||||
onOpenKycClick: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
@ -23,7 +24,7 @@ internal fun TandemPayOnboardingScreen(
|
|||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
onBackClick = {},
|
||||
onBackClick = onBackClick,
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
)
|
||||
},
|
||||
|
|
@ -33,7 +34,7 @@ internal fun TandemPayOnboardingScreen(
|
|||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
state = state,
|
||||
onButtonClick = onButtonClick,
|
||||
onButtonClick = onOpenKycClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -44,6 +45,6 @@ internal fun TandemPayOnboardingScreen(
|
|||
@Composable
|
||||
private fun PreviewDarkTheme() {
|
||||
TangemThemePreview {
|
||||
TandemPayOnboardingScreen(state = TangemPayOnboardingScreenState(), {})
|
||||
TandemPayOnboardingScreen(state = TangemPayOnboardingScreenState(), {}, {})
|
||||
}
|
||||
}
|
||||
|
|
@ -116,6 +116,7 @@ dependencies {
|
|||
implementation(projects.features.kyc.api)
|
||||
implementation(projects.features.tokenRecieve.api)
|
||||
implementation(projects.features.yieldSupply.api)
|
||||
implementation(projects.features.tangempay.details.api)
|
||||
|
||||
/** Common modules */
|
||||
implementation(projects.common)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency
|
|||
import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase
|
||||
import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.settings.*
|
||||
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
|
||||
import com.tangem.domain.wallets.usecase.*
|
||||
|
|
@ -33,12 +34,14 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TangemPayStateConverter
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
|
||||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.*
|
||||
|
|
@ -84,6 +87,9 @@ internal class WalletModel @Inject constructor(
|
|||
private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
|
||||
private val tangemPayStateConverter: TangemPayStateConverter,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
val screenLifecycleProvider: ScreenLifecycleProvider,
|
||||
val innerWalletRouter: InnerWalletRouter,
|
||||
) : Model() {
|
||||
|
|
@ -112,6 +118,7 @@ internal class WalletModel @Inject constructor(
|
|||
subscribeOnSelectedWalletFlow()
|
||||
subscribeToScreenBackgroundState()
|
||||
subscribeOnPushNotificationsPermission()
|
||||
subscribeToTangemPayInfo()
|
||||
enableNotificationsIfNeeded()
|
||||
|
||||
clickIntents.initialize(innerWalletRouter, modelScope)
|
||||
|
|
@ -313,6 +320,27 @@ internal class WalletModel @Inject constructor(
|
|||
.saveIn(clearNFTCacheJobHolder)
|
||||
}
|
||||
|
||||
private fun subscribeToTangemPayInfo() {
|
||||
/**
|
||||
* Update state each time a user opens/returns to wallet screen
|
||||
*/
|
||||
screenLifecycleProvider.isBackgroundState.onEach { isBackground ->
|
||||
if (!isBackground) { updateTangemPayInfo() }
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun updateTangemPayInfo() {
|
||||
if (tangemPayFeatureToggles.isTangemPayEnabled) {
|
||||
modelScope.launch {
|
||||
tangemPayMainScreenCustomerInfoUseCase()?.let { info ->
|
||||
stateHolder.update {
|
||||
it.copy(tangemPayState = tangemPayStateConverter.convert(info))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun needToRefreshTimer() {
|
||||
modelScope.launch {
|
||||
delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class TangemPayState {
|
||||
|
||||
object Empty : TangemPayState()
|
||||
|
||||
data class Progress(
|
||||
val title: TextReference,
|
||||
val buttonText: TextReference,
|
||||
@DrawableRes val iconRes: Int,
|
||||
val onButtonClick: () -> Unit,
|
||||
) : TangemPayState()
|
||||
|
||||
data class Card(
|
||||
val lastFourDigits: TextReference,
|
||||
val balanceText: TextReference,
|
||||
) : TangemPayState()
|
||||
}
|
||||
|
|
@ -14,4 +14,5 @@ internal data class WalletScreenState(
|
|||
val isHidingMode: Boolean,
|
||||
val showMarketsOnboarding: Boolean,
|
||||
val onDismissMarketsOnboarding: () -> Unit,
|
||||
val tangemPayState: TangemPayState = TangemPayState.Empty,
|
||||
)
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.util.Currency
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class TangemPayStateConverter @Inject constructor(
|
||||
private val router: Router,
|
||||
) : Converter<CustomerInfo, TangemPayState> {
|
||||
|
||||
override fun convert(value: CustomerInfo): TangemPayState {
|
||||
val route = AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding)
|
||||
val cardInfo = value.cardInfo
|
||||
return when {
|
||||
!value.isKycApproved() -> TangemPayState.Progress(
|
||||
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),
|
||||
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
|
||||
iconRes = R.drawable.ic_promo_kyc_36,
|
||||
onButtonClick = { router.push(route) },
|
||||
)
|
||||
!value.isProductInstanceActive() -> TangemPayState.Progress(
|
||||
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
|
||||
buttonText = TextReference.Res(R.string.common_continue),
|
||||
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
|
||||
onButtonClick = { router.push(route) },
|
||||
)
|
||||
cardInfo != null -> {
|
||||
TangemPayState.Card(
|
||||
lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"),
|
||||
balanceText = TextReference.Str(getBalanceText(cardInfo)),
|
||||
)
|
||||
}
|
||||
else -> TangemPayState.Empty
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBalanceText(cardInfo: CardInfo): String {
|
||||
val currency = Currency.getInstance(cardInfo.currencyCode)
|
||||
return cardInfo.balance.format {
|
||||
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,6 +79,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
|
|||
import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.BalancesAndLimitsBottomSheet
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDetailsBottomSheet
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
|
||||
|
|
@ -212,6 +213,11 @@ private fun WalletContent(
|
|||
|
||||
notifications(configs = selectedWallet.warnings, modifier = itemModifier)
|
||||
|
||||
item(
|
||||
key = "TangemPayMainScreenBlock",
|
||||
contentType = state.tangemPayState::class.java,
|
||||
) { TangemPayMainScreenBlock(state.tangemPayState, itemModifier) }
|
||||
|
||||
(selectedWallet as? WalletState.SingleCurrency)?.let { walletState ->
|
||||
walletState.marketPriceBlockState?.let { marketPriceBlockState ->
|
||||
marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
|
||||
|
||||
@Composable
|
||||
internal fun TangemPayCardMainBlock(state: TangemPayState.Card, modifier: Modifier = Modifier) {
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
color = TangemTheme.colors.background.primary,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.img_tangem_pay_card_main_36),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(36.dp).clip(CircleShape),
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(12.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.tangempay_kyc_card_ready_notification_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(6.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.img_visa_label_26_16),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(26.dp, 16.dp),
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = state.lastFourDigits.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
contentAlignment = Alignment.TopEnd,
|
||||
) {
|
||||
Text(
|
||||
text = state.balanceText.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.End,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components.visa
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.extensions.TextReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock
|
||||
|
||||
@Composable
|
||||
internal fun TangemPayMainScreenBlock(state: TangemPayState, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(state !is TangemPayState.Empty) {
|
||||
when (state) {
|
||||
is TangemPayState.Progress -> {
|
||||
Notification(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
config = NotificationConfig(
|
||||
title = state.title,
|
||||
iconSize = 36.dp,
|
||||
subtitle = TextReference.EMPTY,
|
||||
iconResId = state.iconRes,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = state.buttonText,
|
||||
onClick = state.onButtonClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
is TangemPayState.Card -> TangemPayCardMainBlock(state, modifier)
|
||||
is TangemPayState.Empty -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ResetCardScreenPreview() {
|
||||
TangemThemePreview {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
|
||||
TangemPayMainScreenBlock(
|
||||
TangemPayState.Progress(
|
||||
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),
|
||||
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
|
||||
iconRes = R.drawable.ic_promo_kyc_36,
|
||||
onButtonClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
TangemPayMainScreenBlock(
|
||||
TangemPayState.Progress(
|
||||
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
|
||||
buttonText = TextReference.Res(R.string.common_continue),
|
||||
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
|
||||
onButtonClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
TangemPayMainScreenBlock(
|
||||
TangemPayState.Card(
|
||||
lastFourDigits = TextReference.Str("*1234"),
|
||||
balanceText = TextReference.Str("$ 0.00"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue