Updated on 2026-08-14

This commit is contained in:
Tangem 2022-02-23 12:47:35 +00:00
commit 3aff4e635f
134 changed files with 4150 additions and 612 deletions

View file

@ -38,7 +38,7 @@ android {
}
debug_beta {
initWith release
debuggable false
debuggable true
versionNameSuffix "-beta"
applicationIdSuffix ".debug"
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
@ -78,8 +78,8 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:develop-64'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-131'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-131'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-132'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-132'
// WebView
implementation "androidx.browser:browser:1.3.0"
@ -129,6 +129,15 @@ dependencies {
// animation
implementation "com.airbnb.android:lottie:3.4.0"
// Shopify
implementation('com.shopify.mobilebuysdk:buy3:12.0.0') {
exclude group: "com.shopify.graphql.support"
exclude module: 'joda-time'
}
// Google Pay
implementation 'com.google.android.gms:play-services-wallet:19.1.0'
testImplementation 'junit:junit:4.13.2'
testImplementation "com.google.truth:truth:1.1.3"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'

View file

@ -39,6 +39,9 @@
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:fullBackupContent">
<meta-data
android:name="com.google.android.gms.wallet.api.enabled"
android:value="true" />
<activity
android:name="com.tangem.tap.MainActivity"

@ -1 +1 @@
Subproject commit 42803296690ad6914ec2c59c5fdc3f0fb30f8afc
Subproject commit 180252680e80b88f8fec9add783207d41d30e4e3

View file

@ -18,8 +18,12 @@ import com.tangem.tap.common.redux.global.AndroidResources
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.shop.GooglePayService
import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.CoroutineExceptionHandler
@ -73,8 +77,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
store.dispatch(GlobalAction.SetResources(getAndroidResources()))
store.dispatch(WalletConnectAction.RestoreSessions)
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
GooglePayService(createPaymentsClient(this), this)
)
)
}
private fun getAndroidResources(): AndroidResources {
return AndroidResources(
AndroidResources.RString(
@ -97,7 +107,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isScannedBefore = store.state.globalState.scanResponse != null
val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore)) {
val shopOpened = store.state.shopState.total != null
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore && !shopOpened)) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
}
intentHandler.handleIntent(intent)
@ -140,4 +151,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
snackbar?.dismiss()
snackbar = null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
LOAD_PAYMENT_DATA_REQUEST_CODE -> {
store.dispatch(
ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data)
)
}
}
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.images.PicassoHelper
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.shop.TangemShopService
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader
@ -37,6 +38,7 @@ val store = Store(
lateinit var preferencesStorage: PreferencesStorage
lateinit var currenciesRepository: CurrenciesRepository
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
class TapApplication : Application() {
override fun onCreate() {
@ -72,7 +74,10 @@ class TapApplication : Application() {
val localLoader = FeaturesLocalLoader(this, moshi)
val remoteLoader = FeaturesRemoteLoader(moshi)
val configManager = ConfigManager(localLoader, remoteLoader)
configManager.load { store.dispatch(GlobalAction.SetConfigManager(configManager)) }
configManager.load {
store.dispatch(GlobalAction.SetConfigManager(configManager))
shopService = TangemShopService(this, configManager.config.shopify!!)
}
val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi))
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
}

View file

@ -15,6 +15,7 @@ import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInPro
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.features.wallet.ui.dialogs.SimpleOkDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -48,6 +49,8 @@ class DialogManager : StoreSubscriber<GlobalState> {
if (dialog != null) return
dialog = when (state.dialog) {
is AppDialog.SimpleOkDialog -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is TwinCardsAction.Wallet.ShowInterruptDialog -> CreateWalletInterruptDialog.create(state.dialog, context)

View file

@ -19,6 +19,7 @@ import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCar
import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
@ -69,6 +70,7 @@ fun FragmentActivity.addOnBackPressedDispatcher(
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
AppScreen.Home -> HomeFragment()
AppScreen.Shop -> ShopFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> TwinsCardsFragment()

View file

@ -68,9 +68,16 @@ fun View.hide(invokeBeforeStateChanged: (() -> Unit)? = null) {
this.visibility = View.GONE
}
fun View.makeInvisible() {
if (this.visibility == View.INVISIBLE) return
this.visibility = View.INVISIBLE
fun View.invisible(invisible: Boolean = true, invokeBeforeStateChanged: (() -> Unit)? = null) {
if (invisible) {
if (this.visibility == View.INVISIBLE) return
invokeBeforeStateChanged?.invoke()
this.visibility = View.INVISIBLE
} else {
this.show(invokeBeforeStateChanged)
}
}
fun Context.dpToPixels(dp: Int): Int =

View file

@ -29,6 +29,10 @@ fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition())
TransitionManager.beginDelayedTransition(this, transition)
}
fun View.beginDelayedTransition(transition: Transition = AutoTransition()) {
(this as? ViewGroup)?.beginDelayedTransition(transition)
}
fun ChipGroup.fitChipsByGroupWidth() {
val layoutStateHandler = GlobalLayoutStateHandler(this)
layoutStateHandler.onStateChanged = stateHandler@{

View file

@ -8,18 +8,26 @@ import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
update()
Result.Success(wallet)
if (isDemoWallet()) {
Result.Success(wallet)
} else {
update()
Result.Success(wallet)
}
} catch (exception: Exception) {
Timber.e(exception)
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
Result.Failure(TapError.NoInternetConnection)
} else {

View file

@ -11,6 +11,7 @@ import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOt
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import org.rekotlin.Action
@ -32,7 +33,8 @@ fun appReducer(action: Action, state: AppState?): AppState {
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState)
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
shopState = ShopReducer.reduce(action, state.shopState),
)
}

View file

@ -23,6 +23,8 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWallet
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.shop.redux.ShopMiddleware
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.tokens.redux.TokensMiddleware
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
@ -44,6 +46,7 @@ data class AppState(
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val shopState: ShopState = ShopState(),
) : StateType {
companion object {
@ -62,7 +65,8 @@ data class AppState(
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,
)
}
}

View file

@ -9,6 +9,8 @@ import com.tangem.tap.features.wallet.redux.Currency
interface StateDialog
sealed class AppDialog : StateDialog {
data class SimpleOkDialog(val header: String, val message: String) : AppDialog()
data class SimpleOkDialogRes(val headerId: Int, val messageId: Int) : AppDialog()
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val currency: Currency,

View file

@ -11,6 +11,7 @@ data class NavigationState(
enum class AppScreen {
Home,
Shop,
Disclaimer,
OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther,
Wallet, WalletDetails,

View file

@ -0,0 +1,216 @@
package com.tangem.tap.common.shop
import android.app.Application
import android.content.Intent
import com.google.android.gms.wallet.PaymentData
import com.shopify.buy3.Storefront
import com.tangem.tap.common.shop.data.ProductType
import com.tangem.tap.common.shop.data.TangemProduct
import com.tangem.tap.common.shop.data.TotalSum
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.common.shop.shopify.data.CheckoutItem
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
import java.util.*
class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
private val shopifyService = ShopifyService(application, shopifyShop)
private lateinit var product: Storefront.Product
private val checkouts = mutableMapOf<ProductType, Storefront.Checkout>()
private val variants = mutableMapOf<ProductType, Storefront.ProductVariant>()
private lateinit var googlePayService: GooglePayService
suspend fun getProducts(): Result<List<TangemProduct>> {
val result = shopifyService.getProducts()
result.onSuccess {
product = it.first {
val variantsSku = it.variants.edges.map { it.node.sku }
variantsSku.contains(ProductType.WALLET_2_CARDS.sku) && variantsSku.contains(
ProductType.WALLET_3_CARDS.sku
)
}
product.variants.edges.map { it.node }
.forEach { variant ->
if (variant.sku == ProductType.WALLET_2_CARDS.sku) {
variants[ProductType.WALLET_2_CARDS] = variant
} else if (variant.sku == ProductType.WALLET_3_CARDS.sku) {
variants[ProductType.WALLET_3_CARDS] = variant
}
}
val twoCardsProduct = TangemProduct(
type = ProductType.WALLET_2_CARDS,
totalSum = TotalSum(
finalValue = variants[ProductType.WALLET_2_CARDS]?.priceV2?.format(),
beforeDiscount = variants[ProductType.WALLET_2_CARDS]?.compareAtPriceV2?.format()
)
)
val threeCardsProduct = TangemProduct(
type = ProductType.WALLET_3_CARDS,
totalSum = TotalSum(
finalValue = variants[ProductType.WALLET_3_CARDS]?.priceV2?.format(),
beforeDiscount = variants[ProductType.WALLET_3_CARDS]?.compareAtPriceV2?.format()
)
)
createCheckouts()
return Result.success(listOf(twoCardsProduct, threeCardsProduct))
}
return Result.failure(result.exceptionOrNull()!!)
}
private suspend fun createCheckouts() {
variants.keys.map { coroutineScope { async { createCheckout(it) } } }.awaitAll()
}
private suspend fun createCheckout(productType: ProductType) {
val checkoutItem = CheckoutItem(variants[productType]!!.id, 1)
val result = shopifyService.createCheckout(listOf(checkoutItem))
result.onSuccess { checkout ->
checkouts[productType] = checkout
}
}
suspend fun checkIfGooglePayAvailable(googlePayService: GooglePayService): Result<Boolean> {
this.googlePayService = googlePayService
return googlePayService.checkIfGooglePayAvailable()
}
fun buyWithGooglePay(productType: ProductType) {
val totalPrice = checkouts[productType]!!.totalPriceV2.amount
googlePayService.payWithGooglePay(
totalPriceCents = totalPrice, currencyCode = checkouts[productType]!!.currencyCode.name,
merchantID = shopifyService.shop.merchantID
)
}
// fun subscribeToGooglePayResult(
// productType: ProductType,
// resultCallback: (Result<PaymentData>) -> Unit
// ) {
// googlePayService.responseCallback = { result ->
// result.onFailure { }
// result.onSuccess {
// completeTokenizedPayment(it, productType)
// }
// }
// }
suspend fun handleGooglePayResult(
resultCode: Int,
data: Intent?,
productType: ProductType
): Result<Unit> {
val result = googlePayService.handleResponseFromGooglePay(resultCode, data)
result.onSuccess {
val finalizePaymentResult = completeTokenizedPayment(it, productType)
finalizePaymentResult.onSuccess {
return Result.success(Unit)
}
return Result.failure(finalizePaymentResult.exceptionOrNull()!!)
}
return Result.failure(result.exceptionOrNull()!!)
}
private suspend fun completeTokenizedPayment(
paymentData: PaymentData,
productType: ProductType
): Result<Storefront.Checkout> {
val checkout = checkouts[productType]!!
val googlePayResponse =
googlePayService.parsePaymentData(paymentData)
?: return Result.failure(Exception("cannot parse GPay result"))
val amount =
Storefront.MoneyInput(checkout.totalPriceV2.amount, checkout.totalPriceV2.currencyCode)
val idempotencyKey = UUID.randomUUID().toString()
val addressGPay = googlePayResponse.billingAddress
val address = Storefront.MailingAddressInput().apply {
lastName = addressGPay.name
address1 = addressGPay.address1
address2 = addressGPay.address2 + addressGPay.address3
province = addressGPay.administrativeArea
zip = addressGPay.postalCode
phone = addressGPay.phoneNumber
}
val payment = Storefront.TokenizedPaymentInputV3(
amount,
idempotencyKey,
address,
paymentData.toJson(),
Storefront.PaymentTokenType.GOOGLE_PAY
)
.setTest(true)
return shopifyService.completeWithTokenizedPayment(
payment = payment,
checkoutID = checkout.id
)
}
suspend fun applyPromoCode(promoCode: String): Result<List<TangemProduct>> {
val products = variants.keys
.map { coroutineScope { async { applyPromoCode(promoCode, it) } } }
.awaitAll()
.map { result -> result.getOrElse { return Result.failure(it) } }
return Result.success(products)
}
suspend fun applyPromoCode(promoCode: String, productType: ProductType): Result<TangemProduct> {
val checkout = checkouts[productType] ?: return Result.failure(Exception("No checkout"))
val result = if (promoCode.isBlank()) {
shopifyService.removeDiscount(checkout.id)
} else {
shopifyService.applyDiscount(promoCode, checkout.id)
}
result.onSuccess {
checkouts[productType] = it
return Result.success(
TangemProduct(
productType,
TotalSum(
finalValue = it.totalPriceV2.format(),
beforeDiscount = variants[productType]!!.compareAtPriceV2.format()
),
appliedDiscount = it.getAppliedDiscount()
)
)
}
return Result.failure(result.exceptionOrNull()!!)
}
fun getCheckoutUrl(productType: ProductType): String {
return checkouts[productType]!!.webUrl
}
companion object {
const val TANGEM_WALLET_2_CARDS_SKU = "TG115x2"
const val TANGEM_WALLET_3_CARDS_SKU = "TG115x3"
}
}
private fun Storefront.MoneyV2.format(): String {
val currencySymbol = Currency.getInstance(currencyCode.name).symbol
val amountFormatted = BigDecimal(amount).setScale(2)
return currencySymbol + amountFormatted
}
private fun Storefront.Checkout.getAppliedDiscount(): String? {
val discountApplication =
discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication
return discountApplication?.code
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.common.shop.data
import com.tangem.tap.common.shop.TangemShopService
enum class ProductType(val sku: String) {
WALLET_2_CARDS(TangemShopService.TANGEM_WALLET_2_CARDS_SKU),
WALLET_3_CARDS(TangemShopService.TANGEM_WALLET_3_CARDS_SKU)
}

View file

@ -0,0 +1,7 @@
package com.tangem.tap.common.shop.data
data class TangemProduct(
val type: ProductType,
val totalSum: TotalSum? = null,
val appliedDiscount: String? = null
)

View file

@ -0,0 +1,6 @@
package com.tangem.tap.common.shop.data
data class TotalSum(
val finalValue: String? = null,
val beforeDiscount: String? = null,
)

View file

@ -0,0 +1,148 @@
package com.tangem.tap.common.shop
import android.app.Activity
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.util.Log
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.wallet.*
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.shop.googlepay.GooglePayUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class GooglePayService(private val paymentsClient: PaymentsClient, private val activity: Activity) {
// var responseCallback: ((Result<PaymentData>) -> Unit)? = null
suspend fun checkIfGooglePayAvailable(): Result<Boolean> {
val isReadyToPayJson = GooglePayUtil.isReadyToPayRequest() ?: return Result.success(false)
val request = IsReadyToPayRequest.fromJson(isReadyToPayJson.toString())
val task = paymentsClient.isReadyToPay(request)
return withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
task.addOnCompleteListener { completedTask ->
try {
val result = completedTask.getResult(ApiException::class.java)
continuation.resume(Result.success(true))
} catch (exception: ApiException) {
// Process error
Timber.w("isReadyToPay failed: $exception")
continuation.resume(Result.failure(exception))
}
}
}
}
}
fun payWithGooglePay(totalPriceCents: String, currencyCode: String, merchantID: String) {
val paymentDataRequestJson = GooglePayUtil.getPaymentDataRequest(
totalPriceCents,
currencyCode = currencyCode,
countryCode = "RU",
merchantID = merchantID
)
if (paymentDataRequestJson == null) {
Timber.e("RequestPayment: can't fetch payment data request")
return
}
val request = PaymentDataRequest.fromJson(paymentDataRequestJson.toString())
AutoResolveHelper.resolveTask(
paymentsClient.loadPaymentData(request), activity, LOAD_PAYMENT_DATA_REQUEST_CODE
)
}
fun handleResponseFromGooglePay(resultCode: Int, data: Intent?): Result<PaymentData> {
val result = when (resultCode) {
RESULT_OK -> {
val paymentData = data?.let { intent -> PaymentData.getFromIntent(intent) }
if (paymentData == null) {
Result.failure(Exception("No payment data"))
} else {
Result.success(paymentData)
}
}
RESULT_CANCELED -> {
Result.failure(TangemSdkError.UserCancelled())
}
AutoResolveHelper.RESULT_ERROR -> {
val statusCode = AutoResolveHelper.getStatusFromIntent(data)?.statusCode
if (statusCode == null) {
Result.failure(Exception("Unknown Status"))
} else {
Result.failure(Exception("$statusCode"))
}
}
else -> Result.failure(Exception("Unknown Status"))
}
// responseCallback?.invoke(result)
return result
}
fun parsePaymentData(paymentData: PaymentData): GooglePayResponse? {
val paymentInformation = paymentData.toJson()
try {
// Token will be null if PaymentDataRequest was not constructed using fromJson(String).
val paymentMethodData =
JSONObject(paymentInformation).getJSONObject("paymentMethodData")
val addressJson = paymentMethodData.getJSONObject("info")
.getJSONObject("billingAddress")
val address = Address(
name = addressJson.getString("name"),
postalCode = addressJson.getString("postalCode"),
countryCode = addressJson.getString("countryCode"),
phoneNumber = addressJson.getString("phoneNumber"),
address1 = addressJson.getString("address1"),
address2 = addressJson.getString("address2"),
address3 = addressJson.getString("address3"),
locality = addressJson.getString("locality"),
administrativeArea = addressJson.getString("administrativeArea"),
sortingCode = addressJson.getString("sortingCode"),
)
val token = paymentMethodData
.getJSONObject("tokenizationData")
.getString("token")
return GooglePayResponse(address, token)
} catch (e: JSONException) {
Log.e("handlePaymentSuccess", "Error: " + e.toString())
}
return null
}
companion object {
const val LOAD_PAYMENT_DATA_REQUEST_CODE = 315
}
}
data class GooglePayResponse(
val billingAddress: Address,
val token: String,
)
data class Address(
val name: String,
val postalCode: String,
val countryCode: String,
val phoneNumber: String,
val address1: String,
val address2: String,
val address3: String,
val locality: String,
val administrativeArea: String,
val sortingCode: String
)

View file

@ -0,0 +1,137 @@
package com.tangem.tap.common.shop.googlepay
import android.app.Activity
import com.google.android.gms.wallet.PaymentsClient
import com.google.android.gms.wallet.Wallet
import com.google.android.gms.wallet.WalletConstants
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
object GooglePayUtil {
private val baseRequest = JSONObject().apply {
put("apiVersion", 2)
put("apiVersionMinor", 0)
}
private fun gatewayTokenizationSpecification(merchantID: String): JSONObject {
return JSONObject().apply {
put("type", "PAYMENT_GATEWAY")
put(
"parameters", JSONObject(
mapOf(
"gateway" to "shopify",
"gatewayMerchantId" to merchantID
)
)
)
}
}
private val allowedCardNetworks = JSONArray(
listOf(
"AMEX",
"DISCOVER",
"INTERAC",
"JCB",
"MASTERCARD",
"VISA"
)
)
private val allowedCardAuthMethods = JSONArray(
listOf(
"PAN_ONLY",
"CRYPTOGRAM_3DS"
)
)
private fun baseCardPaymentMethod(): JSONObject {
return JSONObject().apply {
val parameters = JSONObject().apply {
put("allowedAuthMethods", allowedCardAuthMethods)
put("allowedCardNetworks", allowedCardNetworks)
put("billingAddressRequired", true)
put("billingAddressParameters", JSONObject().apply {
put("format", "FULL")
})
}
put("type", "CARD")
put("parameters", parameters)
}
}
private fun cardPaymentMethod(merchantID: String): JSONObject {
val cardPaymentMethod = baseCardPaymentMethod()
cardPaymentMethod.put("tokenizationSpecification", gatewayTokenizationSpecification(merchantID))
return cardPaymentMethod
}
fun createPaymentsClient(activity: Activity): PaymentsClient {
val walletOptions = Wallet.WalletOptions.Builder()
.setEnvironment(PAYMENTS_ENVIRONMENT)
.build()
return Wallet.getPaymentsClient(activity, walletOptions)
}
fun isReadyToPayRequest(): JSONObject? {
return try {
baseRequest.apply {
put("allowedPaymentMethods", JSONArray().put(baseCardPaymentMethod()))
}
} catch (e: JSONException) {
null
}
}
private fun getTransactionInfo(
price: String,
countryCode: String,
currencyCode: String
): JSONObject {
return JSONObject().apply {
put("totalPrice", price)
put("totalPriceStatus", "FINAL")
put("countryCode", countryCode)
put("currencyCode", currencyCode)
}
}
private val merchantInfo: JSONObject =
JSONObject().put("merchantName", "Example Merchant")
fun getPaymentDataRequest(
price: String,
countryCode: String,
currencyCode: String,
merchantID: String
): JSONObject? {
try {
return baseRequest.apply {
put("allowedPaymentMethods", JSONArray().put(cardPaymentMethod(merchantID)))
put("transactionInfo", getTransactionInfo(price, countryCode, currencyCode))
put("merchantInfo", merchantInfo)
// An optional shipping address requirement is a top-level property of the
// PaymentDataRequest JSON object.
val shippingAddressParameters = JSONObject().apply {
put("phoneNumberRequired", false)
// put("allowedCountryCodes", JSONArray(listOf("US", "GB")))
}
put("shippingAddressParameters", shippingAddressParameters)
put("shippingAddressRequired", true)
}
} catch (e: JSONException) {
return null
}
}
}
const val PAYMENTS_ENVIRONMENT = WalletConstants.ENVIRONMENT_TEST

View file

@ -0,0 +1,384 @@
package com.tangem.tap.common.shop
import android.app.Application
import com.shopify.buy3.GraphCallResult
import com.shopify.buy3.GraphClient
import com.shopify.buy3.RetryHandler
import com.shopify.buy3.Storefront.*
import com.shopify.graphql.support.ID
import com.shopify.graphql.support.Input
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.common.shop.shopify.data.CheckoutItem
import com.tangem.tap.common.shop.shopify.data.checkoutFieldsFragment
import com.tangem.tap.common.shop.shopify.data.collectionFieldsFragment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ShopifyService(private val application: Application, val shop: ShopifyShop) {
val client: GraphClient by lazy { initClient() }
suspend fun getShopName(): Result<String> {
val query = query { rootQuery: QueryRootQuery ->
rootQuery
.shop { shopQuery: ShopQuery ->
shopQuery
.name()
}
}
return when (val result = queryAsync(query)) {
is GraphCallResult.Success -> {
val name = result.response.data!!.shop.name
Result.success(name)
}
is GraphCallResult.Failure -> {
Result.failure(result.error)
}
}
}
suspend fun getProducts(collectionTitleFilter: String? = null): Result<List<Product>> {
val filter = collectionTitleFilter?.let { "title:\"$it\"" }
val query = query { rootQuery: QueryRootQuery ->
rootQuery
.collections(
{ arg -> arg.first(250).query(filter) },
) { collectionConnectionQuery ->
collectionConnectionQuery.collectionFieldsFragment()
}
}
return when (val result = queryAsync(query)) {
is GraphCallResult.Success -> {
val products = result.response.data!!.collections.edges
.map { it.node.products }
.flatMap { it.edges }
.map { it.node }
Result.success(products)
}
is GraphCallResult.Failure -> {
Result.failure(result.error)
}
}
}
suspend fun checkout(pollUntilOrder: Boolean, checkoutID: ID): Result<Checkout> {
val query = query { rootQuery: QueryRootQuery ->
rootQuery
.node(checkoutID) { query ->
query.onCheckout { checkoutQuery ->
with(checkoutQuery) {
checkoutFieldsFragment()
}
}
}
}
val retryHandler = RetryHandler.build<QueryRoot>(
1, TimeUnit.SECONDS
) {
this.retryWhen { result ->
when (result) {
is GraphCallResult.Success -> {
val checkout = result.response.data?.node as? Checkout
checkout == null
}
is GraphCallResult.Failure -> false
}
}
}
val result = if (pollUntilOrder) queryAsync(query, retryHandler) else queryAsync(query)
return when (result) {
is GraphCallResult.Success -> {
val checkout = result.response.data!!.node as? Checkout
if (checkout != null) {
Result.success(checkout)
} else {
Result.failure(ShopifyError.Unknown)
}
}
is GraphCallResult.Failure -> {
Result.failure(result.error)
}
}
}
suspend fun createCheckout(
checkoutItems: List<CheckoutItem>,
checkoutID: ID? = null
): Result<Checkout> {
val storefrontLineItems: MutableList<CheckoutLineItemInput> = checkoutItems
.map { CheckoutLineItemInput(it.quantity, it.id) }.toMutableList()
val query = if (checkoutID != null) {
mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutLineItemsReplace(
storefrontLineItems, checkoutID
) { payloadQuery: CheckoutLineItemsReplacePayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.userErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
} else {
val input = CheckoutCreateInput()
.setLineItemsInput(
Input.value(storefrontLineItems)
)
mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutCreate(
input
) { payloadQuery: CheckoutCreatePayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
}
return runCheckoutMutation(query)
}
suspend fun applyDiscount(discountCode: String, checkoutID: ID): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutDiscountCodeApplyV2(
discountCode, checkoutID
) { payloadQuery: CheckoutDiscountCodeApplyV2PayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun removeDiscount(checkoutID: ID): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutDiscountCodeRemove(
checkoutID
) { payloadQuery: CheckoutDiscountCodeRemovePayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun updateAddress(
address: MailingAddress,
checkoutID: ID,
waitForShippingRates: Boolean
): Result<Checkout> {
val input = MailingAddressInput()
.setAddress1(address.address1)
.setAddress2(address.address2)
.setCity(address.city)
.setCountry(address.country)
.setFirstName(address.firstName)
.setLastName(address.lastName)
.setPhone(address.phone)
.setProvince(address.province)
.setZip(address.zip)
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutShippingAddressUpdateV2(
input, checkoutID
) { shippingAddressUpdatePayloadQuery: CheckoutShippingAddressUpdateV2PayloadQuery ->
shippingAddressUpdatePayloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun updateEmail(email: String?, checkoutID: ID): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutEmailUpdateV2(
checkoutID, email
) { emailUpdatePayloadQuery: CheckoutEmailUpdateV2PayloadQuery ->
emailUpdatePayloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun updateShippingRate(handle: String?, checkoutID: ID): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutShippingLineUpdate(
checkoutID, handle
) { shippingLineUpdatePayloadQuery: CheckoutShippingLineUpdatePayloadQuery ->
shippingLineUpdatePayloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun completeWithTokenizedPayment(
payment: TokenizedPaymentInputV3,
checkoutID: ID
): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutCompleteWithTokenizedPaymentV3(
checkoutID, payment
) { payloadQuery: CheckoutCompleteWithTokenizedPaymentV3PayloadQuery ->
payloadQuery
.payment { paymentQuery: PaymentQuery ->
paymentQuery
.ready()
.errorMessage()
}
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery
.ready()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
fun startGooglePaySession() {
// PaySession()
}
private suspend fun runCheckoutMutation(mutation: MutationQuery): Result<Checkout> {
return when (val result = mutationQueryAsync(mutation)) {
is GraphCallResult.Success -> {
val checkout = result.response.data!!.checkoutCreate?.checkout
?: result.response.data!!.checkoutDiscountCodeApplyV2?.checkout
?: result.response.data!!.checkoutDiscountCodeRemove?.checkout
?: result.response.data!!.checkoutShippingAddressUpdateV2?.checkout
?: result.response.data!!.checkoutEmailUpdateV2?.checkout
?: result.response.data!!.checkoutShippingLineUpdate?.checkout
?: result.response.data!!.checkoutCompleteWithTokenizedPaymentV3.checkout
Result.success(checkout)
}
is GraphCallResult.Failure -> Result.failure(result.error)
}
}
private suspend fun queryAsync(
query: QueryRootQuery,
retryHandler: RetryHandler<QueryRoot>
): GraphCallResult<QueryRoot> =
withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
client.queryGraph(query).enqueue(retryHandler = retryHandler) { result ->
continuation.resume(result)
}
}
}
private suspend fun queryAsync(
query: QueryRootQuery,
): GraphCallResult<QueryRoot> =
withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
client.queryGraph(query).enqueue { result ->
continuation.resume(result)
}
}
}
private suspend fun mutationQueryAsync(query: MutationQuery): GraphCallResult<Mutation> =
withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
client.mutateGraph(query).enqueue { result ->
continuation.resume(result)
}
}
}
private fun initClient(): GraphClient {
return GraphClient.build(
application,
shop.domain,
shop.storefrontApiKey
) {
// httpCache(application.filesDir) {
// cacheMaxSizeBytes = (1024 * 1024 * 10)
// defaultCachePolicy =
// HttpCachePolicy.Default.CACHE_FIRST.expireAfter(20, TimeUnit.MINUTES)
// }
}
}
}
sealed class ShopifyError : Throwable() {
object Unknown : ShopifyError()
object GooglePayFailed : ShopifyError()
class UserError(val errorMessage: String)
}

View file

@ -0,0 +1,10 @@
package com.tangem.tap.common.shop.shopify
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ShopifyShop(
val domain: String,
val storefrontApiKey: String,
val merchantID: String,
)

View file

@ -0,0 +1,134 @@
package com.tangem.tap.common.shop.shopify.data
import com.shopify.buy3.Storefront
fun Storefront.CheckoutQuery.checkoutFieldsFragment() {
// id()
ready()
webUrl()
currencyCode()
lineItemsSubtotalPrice { it.amount() }
totalPriceV2 {
it.currencyCode()
it.amount() }
lineItems({ arg -> arg.first(250) }) {
it.edges {
it.node {
// it.id()
it.title()
it.quantity()
it.variant() {
it.priceV2() { it.amount() }
}
}
}
}
shippingLine {
it.handle()
it.title()
it.priceV2 { it.amount() }
}
availableShippingRates {
it.ready()
it.shippingRates {
it.handle()
it.title()
it.priceV2 { it.amount() }
}
}
shippingAddress {
it.address1()
it.address2()
it.city()
// .company()
it.country()
// .countryCodeV2()
it.firstName()
// .formatted()
// .formattedArea()
// .id()
it.lastName()
// .latitude()
// .longitude()
// .name()
it.phone()
it.province()
// .provinceCode()
it.zip()
}
discountApplications({ arg -> arg.first(250) }) {
it.edges {
it.node {
it.onDiscountCodeApplication {
it.code()
// .applicable()
// .allocationMethod()
// .targetSelection()
// .targetType()
it.value {
it.onMoneyV2 {
it.amount()
}
it.onPricingPercentageValue {
it.percentage()
}
}
}
}
}
}
order {
it.cancelReason()
it.canceledAt()
it.currencyCode()
// .currentSubtotalPrice()
// .currentTotalDuties()
// .currentTotalPrice()
// .currentTotalTax()
it.customerLocale()
it.customerUrl()
// .discountApplications()
it.edited()
it.email()
it.financialStatus()
it.fulfillmentStatus()
// .id()
// .lineItems()
// .metafield()
// .metafields()
it.name()
it.orderNumber()
// .originalTotalDuties()
// .originalTotalPrice()
it.phone()
it.processedAt()
it.shippingAddress {
it.address1()
it.address2()
it.city()
it.company()
it.country()
it.countryCodeV2()
it.firstName()
it.formatted()
it.formattedArea()
// .id()
it.lastName()
it.latitude()
it.longitude()
it.name()
it.phone()
it.province()
it.provinceCode()
it.zip()
}
// .shippingDiscountAllocations()
it.statusUrl()
// .subtotalPriceV2()
// .successfulFulfillments()
// .totalPriceV2()
// .totalRefundedV2()
// .totalShippingPriceV2()
// .totalTaxV2()
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.common.shop.shopify.data
import com.shopify.graphql.support.ID
data class CheckoutItem(
val id: ID,
val quantity: Int
)

View file

@ -0,0 +1,50 @@
package com.tangem.tap.common.shop.shopify.data
import com.shopify.buy3.Storefront
fun Storefront.CollectionConnectionQuery.collectionFieldsFragment() {
edges { collectionEdgeQuery ->
collectionEdgeQuery
.node { collectionQuery ->
collectionQuery
.title()
.products({ arg -> arg.first(250) }
) { productConnectionQuery ->
productConnectionQuery
.edges { productEdgeQuery ->
productEdgeQuery
.node { productQuery ->
productQuery.title()
.productType()
.description()
.variants({ arg -> arg.first(10) }) { variantConnectionQuery ->
variantConnectionQuery.edges { variantQuery ->
variantQuery.node {
it.title()
it.sku()
it.currentlyNotInStock()
it.priceV2 {
it.amount()
it.currencyCode()
}
it.compareAtPriceV2 {
it.amount()
it.currencyCode()
}
it.compareAtPriceV2 {
it.amount()
}
it.product {
it.title()
.productType()
.description()
}
}
}
}
}
}
}
}
}
}

View file

@ -10,9 +10,11 @@ import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.*
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
@ -207,6 +209,9 @@ class TapWalletManager {
data.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
WalletAction.LoadData.Failure(TapError.UnknownBlockchain)
}
data.isDemoCard() -> {
return null
}
data.card.wallets.isEmpty() -> {
WalletAction.EmptyWallet
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.domain.configurable.config
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.domain.configurable.Loader
/**
@ -16,7 +17,8 @@ data class Config(
val isSendingToPayIdEnabled: Boolean = true,
val isTopUpEnabled: Boolean = false,
@Deprecated("Not relevant since version 3.23")
val isCreatingTwinCardsAllowed: Boolean = false
val isCreatingTwinCardsAllowed: Boolean = false,
val shopify: ShopifyShop? = null
)
class ConfigManager(
@ -52,10 +54,10 @@ class ConfigManager(
fun resetToDefault(name: String) {
when (name) {
isSendingToPayIdEnabled -> config =
config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
isCreatingTwinCardsAllowed -> config =
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
}
}
@ -87,6 +89,7 @@ class ConfigManager(
infuraProjectId = values.infuraProjectId
),
appsFlyerDevKey = values.appsFlyerDevKey,
shopify = values.shopifyShop,
)
defaultConfig = defaultConfig.copy(
coinMarketCapKey = values.coinMarketCapKey,
@ -99,6 +102,7 @@ class ConfigManager(
infuraProjectId = values.infuraProjectId
),
appsFlyerDevKey = values.appsFlyerDevKey,
shopify = values.shopifyShop,
)
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.domain.configurable.config
import com.tangem.tap.common.shop.shopify.ShopifyShop
/**
[REDACTED_AUTHOR]
*/
@ -20,6 +22,7 @@ class ConfigValueModel(
val blockcypherTokens: Set<String>?,
val infuraProjectId: String?,
val appsFlyerDevKey: String,
val shopifyShop: ShopifyShop?
)
class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {

View file

@ -10,7 +10,7 @@ import com.tangem.wallet.R
[REDACTED_AUTHOR]
*/
class WarningMessagesManager(
private val warningLoader: RemoteWarningLoader,
private val warningLoader: RemoteWarningLoader,
) {
private val warningsList: MutableList<WarningMessage> = mutableListOf()
@ -33,15 +33,15 @@ class WarningMessagesManager(
fun getWarnings(location: WarningMessage.Location, forBlockchains: List<Blockchain> = emptyList()): List<WarningMessage> {
return warningsList
.filter { !it.isHidden && it.location.contains(location) }
.filter {
val list = it.blockchainList
when {
list == null -> true
list.containsAny(forBlockchains) -> true
else -> false
}
.filter { !it.isHidden && it.location.contains(location) }
.filter {
val list = it.blockchainList
when {
list == null -> true
list.containsAny(forBlockchains) -> true
else -> false
}
}
}
fun hideWarning(warning: WarningMessage): Boolean {
@ -49,7 +49,7 @@ class WarningMessagesManager(
return when {
foundWarning == null -> false
foundWarning.type == WarningMessage.Type.Temporary
|| foundWarning.type == WarningMessage.Type.AppRating -> {
|| foundWarning.type == WarningMessage.Type.AppRating -> {
if (foundWarning.isHidden) {
false
} else {
@ -80,32 +80,32 @@ class WarningMessagesManager(
companion object {
fun devCardWarning(): WarningMessage = WarningMessage(
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.alert_developer_card,
WarningMessage.Origin.Local
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.alert_developer_card,
WarningMessage.Origin.Local
)
fun alreadySignedHashesWarning(): WarningMessage = WarningMessage(
"",
"",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.alert_card_signed_transactions,
WarningMessage.Origin.Local
"",
"",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.alert_card_signed_transactions,
WarningMessage.Origin.Local
)
fun signedHashesMultiWalletWarning(): WarningMessage = WarningMessage(
title = "",
message = "",
message = "",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Info,
location = listOf(WarningMessage.Location.MainScreen),
@ -117,15 +117,15 @@ class WarningMessagesManager(
)
fun appRatingWarning(): WarningMessage = WarningMessage(
"",
"",
WarningMessage.Type.AppRating,
WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_rate_app_title,
R.string.warning_rate_app_message,
WarningMessage.Origin.Local
"",
"",
WarningMessage.Type.AppRating,
WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_rate_app_title,
R.string.warning_rate_app_message,
WarningMessage.Origin.Local
)
fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
@ -133,15 +133,15 @@ class WarningMessagesManager(
}
fun onlineVerificationFailed(): WarningMessage = WarningMessage(
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_failed_to_verify_card_title,
R.string.warning_failed_to_verify_card_message,
WarningMessage.Origin.Local
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_failed_to_verify_card_title,
R.string.warning_failed_to_verify_card_message,
WarningMessage.Origin.Local
)
fun remainingSignaturesNotEnough(remainingSignatures: Int): WarningMessage = WarningMessage(
@ -169,6 +169,18 @@ class WarningMessagesManager(
WarningMessage.Origin.Local
)
fun demoCardWarning(): WarningMessage = WarningMessage(
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.alert_demo_message,
WarningMessage.Origin.Local
)
const val REMAINING_SIGNATURES_WARNING = 10
}
}

View file

@ -5,8 +5,10 @@ import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.operations.CommandResponse
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
@ -16,10 +18,12 @@ import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.domain.tasks.product.CreateWalletsTask
import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey
import com.tangem.tap.domain.tasks.product.ProductCommandProcessor
import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets
import com.tangem.tap.features.demo.DemoHelper
data class CreateProductWalletTaskResponse(
@ -96,25 +100,27 @@ private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletRespo
private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWalletTaskResponse> {
private lateinit var card: Card
private var primaryCard: PrimaryCard? = null
private var createWalletResponse: CreateWalletResponse? = null
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
this.card = card
val curves = card.getCurvesForNonCreatedWallets()
CreateWalletsTask(curves).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
createWalletResponse = result.data.createWalletResponses[0]
val createWalletResponses = result.data.createWalletResponses
when {
card.settings.isBackupAllowed -> {
linkPrimaryCard(session, callback)
linkPrimaryCard(createWalletResponses, session, callback)
}
card.settings.isHDWalletAllowed -> {
deriveKeys(session, callback)
deriveKeys(createWalletResponses, session, callback)
}
else -> {
callback(
@ -124,7 +130,6 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
)
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
@ -132,6 +137,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
}
private fun linkPrimaryCard(
createWalletResponse: List<CreateWalletResponse>,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
@ -139,7 +145,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
when (result) {
is CompletionResult.Success -> {
primaryCard = result.data
deriveKeys(session, callback)
deriveKeys(createWalletResponse, session, callback)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
@ -149,28 +155,26 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
}
private fun deriveKeys(
createWalletResponse: List<CreateWalletResponse>,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
val derivationPaths = listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
.mapNotNull { it.derivationPath() }
val response = createWalletResponse.guard {
val map = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
createWalletResponse.forEach { response ->
val blockchainsForCurve = getBlockchains(response.cardId).filter {
it.getSupportedCurves().contains(response.wallet.curve)
}
val derivationPaths = blockchainsForCurve.mapNotNull { it.derivationPath() }
if (derivationPaths.isNotEmpty()) {
map[response.wallet.publicKey.toMapKey()] = derivationPaths
}
}
if (map.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
return
}
if (derivationPaths.isNullOrEmpty()) {
callback(
CompletionResult.Success(
CreateProductWalletTaskResponse(
card = session.environment.card!!, primaryCard = primaryCard
)
)
)
return
}
DeriveMultipleWalletPublicKeysTask(mapOf(response.wallet.publicKey.toMapKey() to derivationPaths))
DeriveMultipleWalletPublicKeysTask(map)
.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
@ -188,4 +192,12 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
}
}
}
private fun getBlockchains(cardId: String): List<Blockchain> {
return when {
DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains
card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet)
else -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
}
}

View file

@ -13,9 +13,11 @@ import com.tangem.tap.common.extensions.appendIf
import com.tangem.tap.common.extensions.readJsonFileToString
import com.tangem.tap.domain.extensions.getCustomIconUrl
import com.tangem.tap.domain.extensions.setCustomIconUrl
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.network.createMoshi
class CurrenciesRepository(val context: Application) {
private val moshi = createMoshi()
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Blockchain::class.java)
@ -28,10 +30,13 @@ class CurrenciesRepository(val context: Application) {
)
fun loadCardCurrencies(cardId: String): CardCurrencies? {
val blockchains = loadSavedBlockchains(cardId)
val blockchains = loadSavedBlockchains(cardId).toMutableSet()
if (DemoHelper.isDemoCardId(cardId)) {
blockchains.addAll(DemoHelper.config.demoBlockchains)
}
if (blockchains.isEmpty()) return null
return CardCurrencies(loadSavedTokens(cardId), blockchains)
return CardCurrencies(loadSavedTokens(cardId), blockchains.toList())
}
fun saveCardCurrencies(cardId: String, currencies: CardCurrencies) {
@ -164,11 +169,10 @@ class CurrenciesRepository(val context: Application) {
return excludeUnsupportedBlockchains(blockchains)
}
//TODO: move to the App settings
private fun excludeUnsupportedBlockchains(blockchains: List<Blockchain>): List<Blockchain> {
return blockchains.toMutableList().apply {
removeAll(listOf(
Blockchain.Fantom, Blockchain.FantomTestnet
// Blockchain.Fantom, Blockchain.FantomTestnet
))
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.Result
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.store
@ -14,7 +15,7 @@ import java.math.BigDecimal
class TopUpManager {
suspend fun topUpTestErc20Tokens(walletManager: EthereumWalletManager, token: Token) {
walletManager.update()
walletManager.safeUpdate()
val amountToSend = Amount(walletManager.wallet.blockchain)
val destinationAddress = token.contractAddress

View file

@ -16,6 +16,7 @@ import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
*/
class TwinsCardWidget(
val leapfrogWidget: LeapfrogWidget,
private val deviceScaleFactor: Float = 1f,
val getTopOfAnchorViewForActivateState: () -> Float
) {
@ -87,20 +88,20 @@ class TwinsCardWidget(
return when (cardNumber) {
TwinCardNumber.First -> {
TwinsCardProperties(
xTranslation = -120f,
yTranslation = -220f,
xTranslation = -120f * deviceScaleFactor,
yTranslation = -220f * deviceScaleFactor,
rotation = -3f,
elevation = 1f,
scale = 1f,
scale = deviceScaleFactor,
)
}
TwinCardNumber.Second -> {
TwinsCardProperties(
xTranslation = 170f,
yTranslation = 170f,
xTranslation = 170f * deviceScaleFactor,
yTranslation = 170f * deviceScaleFactor,
rotation = -3f,
elevation = 0f,
scale = 1f,
scale = deviceScaleFactor,
)
}
}
@ -123,7 +124,7 @@ class TwinsCardWidget(
xTranslation = twinProperties.xTranslation,
yTranslation = twinProperties.yTranslation - topOfAnchorView,
rotation = 0f,
scale = twinProperties.scale - 0.5f,
scale = (twinProperties.scale - 0.5f) * deviceScaleFactor,
)
}

View file

@ -15,6 +15,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
@ -35,18 +36,11 @@ class WalletConnectSdkHelper {
id: Long,
type: WcTransactionType,
): WcTransactionData? {
val walletManager = getWalletManager(session) ?: return null
try {
walletManager.update()
} catch (exception: Exception) {
Timber.e(exception)
return null
}
walletManager.safeUpdate()
val wallet = walletManager.wallet
val balance =
wallet.amounts[AmountType.Coin]?.value ?: return null
val balance = wallet.amounts[AmountType.Coin]?.value ?: return null
val gas = transaction.gas?.hexToBigDecimal()
?: transaction.gasLimit?.hexToBigDecimal()

View file

@ -0,0 +1,153 @@
package com.tangem.tap.features.demo
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
interface DemoMiddleware {
fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean
}
object DemoHelper {
val config = DemoConfig()
private val demoMiddlewares = listOf(
DemoOnboardingNoteMiddleware(),
)
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
WalletAction.TradeCryptoAction.Buy::class.java,
WalletAction.TradeCryptoAction.Sell::class.java,
BackupAction.StartBackup::class.java,
WalletAction.ExploreAddress::class.java
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)
fun isDemoCardId(cardId: String): Boolean = config.isDemoCardId(cardId)
fun tryHandle(appState: () -> AppState?, action: Action): Boolean {
val scanResponse = getScanResponse(appState) ?: return false
if (!scanResponse.isDemoCard()) return false
demoMiddlewares.forEach {
if (it.tryHandle(config, scanResponse, action)) return true
}
disabledActionFeatures.firstOrNull { it == action::class.java }?.let {
store.dispatchNotification(R.string.alert_demo_feature_disabled)
return true
}
return false
}
fun injectDemoBalance(walletManager: WalletManager?) {
val manager = walletManager ?: return
val blockchain = walletManager.wallet.blockchain
val amount = config.getBalance(blockchain)
manager.wallet.setAmount(amount)
}
private fun getScanResponse(appState: () -> AppState?): ScanResponse? {
val state = appState() ?: return null
return state.globalState.onboardingState.onboardingManager?.scanResponse
?: state.globalState.scanResponse
}
}
class DemoConfig {
val demoBlockchains = listOf(
Blockchain.Bitcoin,
Blockchain.Ethereum,
Blockchain.Dogecoin,
Blockchain.Solana,
)
val demoCardIds: List<String> by lazy {
val demoIds = (releaseDemoCardIds + testDemoCardIds).toMutableList()
if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds)
return@lazy demoIds.distinct()
}
private val walletBalances: Map<Blockchain, Amount> = mapOf(
Blockchain.Bitcoin to Amount(0.028.toBigDecimal(), Blockchain.Bitcoin),
Blockchain.Ethereum to Amount(0.2311.toBigDecimal(), Blockchain.Ethereum),
Blockchain.Dogecoin to Amount(1450.025.toBigDecimal(), Blockchain.Dogecoin),
Blockchain.Solana to Amount(13.246.toBigDecimal(), Blockchain.Solana),
)
fun isDemoCardId(cardId: String): Boolean = demoCardIds.contains(cardId)
fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy()
?: Amount(BigDecimal.ZERO, blockchain).copy()
private val releaseDemoCardIds = mutableListOf<String>(
)
private val testDemoCardIds = listOf(
"FB20000000000186", // Note ETH
"FB10000000000196", // Note BTC
"FB30000000000176", // Wallet
//TODO: delete bellow ids before 3.28 release
"AB01000000045060", // Note BTC
"AB02000000045028", // Note ETH
"AC79000000000004", // Wallet 4.46
)
private val debugTestDemoCardIds = listOf(
"AB01000000045060", // Note BTC
"AB02000000045028", // Note ETH
"AC79000000000004", // Wallet 4.46
)
}
class DemoTransactionSender(
private val walletManager: WalletManager,
private val sender: TransactionSender = walletManager as TransactionSender
) : TransactionSender {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> =
sender.getFee(amount, destination)
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val dataToSign = randomString(32).toByteArray()
val signerResponse = signer.sign(dataToSign, walletManager.wallet.cardId, walletManager.wallet.publicKey)
return SimpleResult.Failure(Exception(ID))
}
private fun randomInt(from: Int, to: Int): Int = kotlin.random.Random.nextInt(from, to)
private fun randomString(length: Int): String {
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
return (1..length)
.map { randomInt(0, charPool.size) }
.map(charPool::get)
.joinToString("")
}
companion object {
val ID = DemoTransactionSender::class.java.simpleName
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.tap.features.demo
import com.tangem.common.extensions.guard
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
internal class DemoOnboardingNoteMiddleware : DemoMiddleware {
override fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean {
val globalState = store.state.globalState
val noteState = store.state.onboardingNoteState
when (action) {
is OnboardingNoteAction.Balance.Update -> {
val walletManager = if (noteState.walletManager != null) {
noteState.walletManager
} else {
val wmFactory = globalState.tapWalletManager.walletManagerFactory
val walletManager = wmFactory.makePrimaryWalletManager(scanResponse).guard {
return false
}
store.dispatch(OnboardingNoteAction.SetWalletManager(walletManager))
walletManager
}
val balanceAmount = config.getBalance(walletManager.wallet.blockchain)
val loadedBalance = noteState.walletBalance.copy(
value = balanceAmount.value!!,
currency = Currency.Blockchain(walletManager.wallet.blockchain),
state = ProgressState.Done,
error = null,
criticalError = null
)
walletManager.wallet.setAmount(balanceAmount)
scope.launch {
withMainContext {
store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance))
store.dispatch(OnboardingNoteAction.Balance.SetCriticalError(loadedBalance.criticalError))
store.dispatch(OnboardingNoteAction.Balance.SetNonCriticalError(loadedBalance.error))
}
}
return true
}
}
return false
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.demo
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.tasks.product.ScanResponse
/**
[REDACTED_AUTHOR]
*/
fun ScanResponse.isDemoCard(): Boolean = DemoHelper.isDemoCardId(card.cardId)
fun WalletManager.isDemoWallet(): Boolean = DemoHelper.isDemoCardId(wallet.cardId)
fun Wallet.isDemoWallet(): Boolean = DemoHelper.isDemoCardId(cardId)

View file

@ -17,9 +17,11 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.walletconnect.BnbHelper
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.Action
import org.rekotlin.Middleware
class WalletConnectMiddleware {
@ -28,138 +30,131 @@ class WalletConnectMiddleware {
val walletConnectMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
handle(state, action)
next(action)
}
}
}
is WalletConnectAction.RestoreSessions -> {
walletConnectManager.restoreSessions()
}
private fun handle(state: () -> AppState?, action: Action) {
if (DemoHelper.tryHandle(state, action)) return
is WalletConnectAction.HandleDeepLink -> {
if (!action.wcUri.isNullOrBlank()) {
if (WalletConnectManager.isCorrectWcUri(action.wcUri)) {
store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri))
}
}
}
is WalletConnectAction.StartWalletConnect -> {
val uri = action.activity.getFromClipboard()?.toString()
if (uri != null && WalletConnectManager.isCorrectWcUri(uri)) {
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan))
}
}
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.ClipboardOrScanQr(
action.wcUri
)
)
)
}
is WalletConnectAction.OpeningSessionTimeout -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout))
}
is WalletConnectAction.FailureEstablishingSession -> {
if (action.session != null) {
walletConnectManager.disconnect(action.session)
}
}
is WalletConnectAction.UnsupportedCard -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedCard))
}
is WalletConnectAction.OpenSession -> {
walletConnectManager.connect(
wcUri = action.wcUri,
)
}
is WalletConnectAction.RefuseOpeningSession -> {
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.OpeningSessionRejected
)
)
}
is WalletConnectAction.ScanCard -> {
scanCard(action.session, action.chainId)
}
is WalletConnectAction.ApproveSession -> {
walletConnectManager.approve(action.session)
}
is WalletConnectAction.DisconnectSession -> {
walletConnectManager.disconnect(action.session)
}
is WalletConnectAction.HandleTransactionRequest -> {
walletConnectManager.handleTransactionRequest(
transaction = action.transaction,
session = action.session,
id = action.id,
type = action.type
)
}
is WalletConnectAction.HandlePersonalSignRequest -> {
walletConnectManager.handlePersonalSignRequest(
message = action.message,
session = action.session,
id = action.id
)
}
is WalletConnectAction.RejectRequest -> {
walletConnectManager.rejectRequest(action.session, action.id)
}
is WalletConnectAction.SendTransaction -> {
walletConnectManager.completeTransaction(action.session)
}
is WalletConnectAction.SignMessage -> {
walletConnectManager.sendSignedMessage(action.session)
}
is WalletConnectAction.BinanceTransaction.Trade -> {
val messageData = BnbHelper.createMessageData(action.order)
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.BnbTransactionDialog(
data = messageData,
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name
)
)
)
}
is WalletConnectAction.BinanceTransaction.Transfer -> {
val messageData = BnbHelper.createMessageData(action.order)
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.BnbTransactionDialog(
data = messageData,
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name
)
)
)
}
is WalletConnectAction.BinanceTransaction.Sign -> {
walletConnectManager.signBnb(
action.id, action.data, action.sessionData
)
when (action) {
is WalletConnectAction.RestoreSessions -> {
walletConnectManager.restoreSessions()
}
is WalletConnectAction.HandleDeepLink -> {
if (!action.wcUri.isNullOrBlank()) {
if (WalletConnectManager.isCorrectWcUri(action.wcUri)) {
store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri))
}
}
next(action)
}
is WalletConnectAction.StartWalletConnect -> {
val uri = action.activity.getFromClipboard()?.toString()
if (uri != null && WalletConnectManager.isCorrectWcUri(uri)) {
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan))
}
}
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.ClipboardOrScanQr(
action.wcUri
)
)
)
}
is WalletConnectAction.OpeningSessionTimeout -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout))
}
is WalletConnectAction.FailureEstablishingSession -> {
if (action.session != null) {
walletConnectManager.disconnect(action.session)
}
}
is WalletConnectAction.UnsupportedCard -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedCard))
}
is WalletConnectAction.OpenSession -> {
walletConnectManager.connect(
wcUri = action.wcUri,
)
}
is WalletConnectAction.RefuseOpeningSession -> {
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.OpeningSessionRejected
)
)
}
is WalletConnectAction.ScanCard -> {
scanCard(action.session, action.chainId)
}
is WalletConnectAction.ApproveSession -> {
walletConnectManager.approve(action.session)
}
is WalletConnectAction.DisconnectSession -> {
walletConnectManager.disconnect(action.session)
}
is WalletConnectAction.HandleTransactionRequest -> {
walletConnectManager.handleTransactionRequest(
transaction = action.transaction,
session = action.session,
id = action.id,
type = action.type
)
}
is WalletConnectAction.HandlePersonalSignRequest -> {
walletConnectManager.handlePersonalSignRequest(
message = action.message,
session = action.session,
id = action.id
)
}
is WalletConnectAction.RejectRequest -> {
walletConnectManager.rejectRequest(action.session, action.id)
}
is WalletConnectAction.SendTransaction -> {
walletConnectManager.completeTransaction(action.session)
}
is WalletConnectAction.SignMessage -> {
walletConnectManager.sendSignedMessage(action.session)
}
is WalletConnectAction.BinanceTransaction.Trade -> {
val messageData = BnbHelper.createMessageData(action.order)
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.BnbTransactionDialog(
data = messageData,
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name
)
)
)
}
is WalletConnectAction.BinanceTransaction.Transfer -> {
val messageData = BnbHelper.createMessageData(action.order)
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.BnbTransactionDialog(
data = messageData,
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name
)
)
)
}
is WalletConnectAction.BinanceTransaction.Sign -> {
walletConnectManager.signBnb(
action.id, action.data, action.sessionData
)
}
}
}
@ -190,9 +185,9 @@ class WalletConnectMiddleware {
}
val walletManager = getWalletManager(scanResponse, blockchain).guard {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
return
}
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
return
}
val wallet = walletManager.wallet
val derivedKey =
@ -227,7 +222,7 @@ class WalletConnectMiddleware {
): WalletManager? {
val card = scanResponse.card
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
Blockchain.EthereumTestnet
} else {
blockchain

View file

@ -68,7 +68,7 @@ class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm),
}
ConfirmScreenState.LongTap, ConfirmScreenState.AccessCode,
ConfirmScreenState.PassCode -> {
toolbar.title = getString(R.string.details_row_title_manage_security)
toolbar.title = getString(R.string.details_manage_security_title)
tv_warning_description.text = getString(R.string.details_security_management_warning)
btn_confirm.text = getString(R.string.common_save_changes)
btn_confirm.setCompoundDrawablesRelativeWithIntrinsicBounds(

View file

@ -125,7 +125,7 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
tv_security_title.setOnClickListener {
ll_manage_security.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.CheckCurrentSecurityOption(state.scanResponse!!.card))
}

View file

@ -11,6 +11,9 @@ import com.tangem.common.card.Card
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.domain.TapWorkarounds
import com.tangem.tap.features.feedback.EmailData.Companion.appendBlankLine
import com.tangem.tap.features.feedback.EmailData.Companion.appendDelimiter
import com.tangem.wallet.R
import timber.log.Timber
import java.io.File
import java.io.FileWriter
@ -40,7 +43,8 @@ class FeedbackManager(
val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null
activity.sendEmail(
email = getSupportEmail(),
subject = emailData.subject, message = emailData.joinTogether(infoHolder, emailData !is ScanFailsEmail),
subject = activity.getString(emailData.subjectResId),
message = emailData.joinTogether(activity, infoHolder),
file = fileLog,
onFail = onFail
)
@ -142,7 +146,7 @@ class AdditionalEmailInfo {
cardFirmwareVersion = card.firmwareVersion.stringValue
cardIssuer = card.issuer.name
signedHashesCount = card.wallets
.joinToString(";") { "${it.curve?.curve} - ${it.totalSignedHashes}" }
.joinToString(";") { "${it.curve?.curve} - ${it.totalSignedHashes}" }
}
fun setWalletsInfo(walletManagers: List<WalletManager>) {
@ -201,43 +205,40 @@ class AdditionalEmailInfo {
}
interface EmailData {
val subject: String
val mainMessage: String
val subjectResId: Int
val mainMessageResId: Int
fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message
fun prepare(infoHolder: AdditionalEmailInfo) {}
fun appendDelimiter(builder: StringBuilder) {
builder.append("----------\n")
}
fun appendBlankLine(builder: StringBuilder) {
builder.append("\n")
}
fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String
fun joinTogether(infoHolder: AdditionalEmailInfo, allowToErasePhoneInfo: Boolean): String {
val allowToErasePhoneInfoDisclaimer = if (allowToErasePhoneInfo) {
"Following information is optional. You can erase it if you dont want to share it.\n\n"
} else {
""
}
return "$mainMessage\n\n\n\n" +
allowToErasePhoneInfoDisclaimer +
createOptionalMessage(infoHolder)
fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
append(context.getString(mainMessageResId))
append("\n\n\n\n")
append(context.getString(getDataCollectionMessageResId()))
append(createOptionalMessage(infoHolder))
}.toString()
}
companion object {
internal fun StringBuilder.appendDelimiter() = append("----------\n")
internal fun StringBuilder.appendBlankLine() = append("\n")
}
}
class RateCanBeBetterEmail : EmailData {
override val subject: String = "My suggestions"
override val mainMessage: String = "Tell us what functions you are missing, and we will try to help you."
override val subjectResId: Int = R.string.feedback_subject_rate_negative
override val mainMessageResId: Int = R.string.feedback_preface_rate_negative
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
val walletInfo = infoHolder.walletsInfo[0]
return StringBuilder().apply {
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
appendBlankLine(this)
appendBlankLine()
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
@ -246,11 +247,21 @@ class RateCanBeBetterEmail : EmailData {
}
class ScanFailsEmail : EmailData {
override val subject: String = "Cant scan a card"
override val mainMessage: String = "Please tell us what card do you have?"
override val subjectResId: Int = R.string.feedback_subject_scan_failed
override val mainMessageResId: Int = R.string.feedback_preface_scan_failed
override fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
append(context.getString(mainMessageResId))
append("\n\n\n\n")
append(createOptionalMessage(infoHolder))
}.toString()
}
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendBlankLine(this)
appendBlankLine()
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
@ -259,8 +270,8 @@ class ScanFailsEmail : EmailData {
}
class SendTransactionFailedEmail(private val error: String) : EmailData {
override val subject: String = "Cant send a transaction"
override val mainMessage: String = "Please tell us more about your issue. Every small detail can help."
override val subjectResId: Int = R.string.feedback_subject_tx_failed
override val mainMessageResId: Int = R.string.feedback_preface_tx_failed
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo()
@ -268,17 +279,17 @@ class SendTransactionFailedEmail(private val error: String) : EmailData {
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
appendDelimiter(this)
appendDelimiter()
appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
appendKeyValue("Host", walletInfo.host)
appendKeyValue("Token", infoHolder.token)
appendKeyValue("Error", error)
appendDelimiter(this)
appendDelimiter()
appendKeyValue("Source address", walletInfo.address)
appendKeyValue("Destination address", infoHolder.destinationAddress)
appendKeyValue("Amount", infoHolder.amount)
appendKeyValue("Fee", infoHolder.fee)
appendBlankLine(this)
appendBlankLine()
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
@ -288,16 +299,16 @@ class SendTransactionFailedEmail(private val error: String) : EmailData {
}
class FeedbackEmail : EmailData {
override val subject: String
override val subjectResId: Int
get() = if (isS2CCard) s2cSubject else tangemSubject
override val mainMessage: String
override val mainMessageResId: Int
get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
private val tangemSubject = "Tangem feedback"
private val tangemMainMessage = "Hi support team,"
private val tangemSubject: Int = R.string.feedback_subject_support_tangem
private val tangemMainMessage: Int = R.string.feedback_preface_support
private val s2cSubject = "Feedback"
private val s2cMainMessage = "Hi support team,"
private val s2cSubject: Int = R.string.feedback_subject_support
private val s2cMainMessage: Int = R.string.feedback_preface_support
private var isS2CCard = false
@ -312,26 +323,26 @@ class FeedbackEmail : EmailData {
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
infoHolder.walletsInfo.forEach {
appendDelimiter(builder)
builder.appendDelimiter()
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
builder.appendKeyValue("Host", it.host)
builder.appendKeyValue("Wallet address", it.address)
builder.appendKeyValue("Explorer link", it.explorerLink)
}
appendBlankLine(builder)
builder.appendBlankLine()
infoHolder.tokens.forEach { tokens ->
appendDelimiter(builder)
builder.appendDelimiter()
builder.appendKeyValue("Blockchain", tokens.key.fullName)
builder.appendKeyValue("Tokens", tokens.value.map { "${it.name} - ${it.symbol}" }.toString())
}
appendDelimiter(builder)
appendBlankLine(builder)
builder.appendDelimiter()
builder.appendBlankLine()
// appendKeyValue("Outputs count", infoHolder.outputsCount)
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
builder.appendKeyValue("OS version", infoHolder.osVersion)
builder.appendKeyValue("App version", infoHolder.appVersion)
return builder.toString()
}
}

View file

@ -4,7 +4,6 @@ import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.analytics.GetCardSourceParams
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.post
@ -50,7 +49,7 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
}
is HomeAction.ReadCard -> handleReadCard()
is HomeAction.GoToShop -> {
store.dispatchOpenUrl(HomeMiddleware.CARD_SHOP_URI)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
store.state.globalState.analyticsHandlers?.triggerEvent(
event = AnalyticsEvent.GET_CARD,
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param)

View file

@ -45,7 +45,7 @@ class AddressInfoBottomSheetDialog(
btn_fl_share.setOnClickListener {
store.dispatchShare(data.shareUrl)
}
tv_recieve_message.text = getQRReceiveMessage(tv_recieve_message.context, stateDialog.currency)
tv_receive_message.text = getQRReceiveMessage(tv_receive_message.context, stateDialog.currency)
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.scope
@ -33,14 +34,16 @@ class OnboardingNoteMiddleware {
private val onboardingNoteMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleNoteAction(action, dispatch)
handleNoteAction(state, action, dispatch)
next(action)
}
}
}
private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch: DispatchFunction) {
if (action !is OnboardingNoteAction) return
if (DemoHelper.tryHandle(appState, action)) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager ?: return

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.onboarding.products.twins.ui
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
@ -38,6 +39,7 @@ import kotlinx.android.synthetic.main.view_bg_twins_welcome.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
private var previousStep = TwinCardsStep.None
@ -75,7 +77,14 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
addBackPressHandler(this)
reconfigureLayoutForTwins()
twinsWidget = TwinsCardWidget(LeapfrogWidget(cards_container)) { 285f }
val typedValue = TypedValue()
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
val deviceScaleFactorForWelcomeState = typedValue.float
twinsWidget = TwinsCardWidget(LeapfrogWidget(cards_container), deviceScaleFactorForWelcomeState) {
285f * deviceScaleFactorForWelcomeState
}
btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container)
toolbar.title = getText(R.string.twins_recreate_toolbar)

View file

@ -16,6 +16,7 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware.Companion.BUY_WALLET_URL
import com.tangem.tap.features.wallet.redux.Artwork
@ -164,14 +165,16 @@ class BackupMiddleware {
val backupMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is BackupAction) handleBackupAction(action)
if (action is BackupAction) handleBackupAction(state, action)
next(action)
}
}
}
}
private fun handleBackupAction(action: BackupAction) {
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
if (DemoHelper.tryHandle(appState, action)) return
val backupState = store.state.onboardingWalletState.backupState
val globalState = store.state.globalState

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.os.Bundle
import android.util.TypedValue
import android.view.*
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
@ -13,6 +14,7 @@ import com.squareup.picasso.Picasso
import com.tangem.common.CardIdFormatter
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.PropertyCalculator
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.FragmentOnBackPressedHandler
@ -32,7 +34,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
StoreSubscriber<OnboardingWalletState>, FragmentOnBackPressedHandler {
private var accessCodeDialog: AccessCodeDialog? = null
private lateinit var cardsWidget: BackupCardsWidget
private lateinit var cardsWidget: WalletCardsWidget
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -47,8 +49,15 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val leapfrog = LeapfrogWidget(fl_cards_container)
cardsWidget = BackupCardsWidget(leapfrog) { 200f }
val typedValue = TypedValue()
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
val deviceScaleFactor = typedValue.float
val leapfrogCalculator = PropertyCalculator(
yTranslationFactor = 25f * deviceScaleFactor,
)
val leapfrog = LeapfrogWidget(fl_cards_container, leapfrogCalculator)
cardsWidget = WalletCardsWidget(leapfrog, deviceScaleFactor) { 200f * deviceScaleFactor }
startPostponedEnterTransition()
view_pager_backup_info.adapter = BackupInfoAdapter()
@ -121,8 +130,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
tv_header.setText(R.string.onboarding_create_wallet_header)
tv_body.setText(R.string.onboarding_create_wallet_body)
cardsWidget.toFolded()
startPostponedEnterTransition()
cardsWidget.toFolded(false) { startPostponedEnterTransition() }
}
@ -379,7 +387,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
val shopMenuShouldBeVisible =
(backupStep == BackupStep.ScanOriginCard || backupStep == BackupStep.AddBackupCards) &&
backupState.buyAdditionalCardsUrl != null
backupState.buyAdditionalCardsUrl != null
menu.getItem(0).isVisible = shopMenuShouldBeVisible
}

View file

@ -10,8 +10,9 @@ import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapView
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapViewState
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
class BackupCardsWidget(
class WalletCardsWidget(
val leapfrogWidget: LeapfrogWidget,
private val deviceScaleFactor: Float = 1f,
val getTopOfAnchorViewForActivateState: () -> Float,
) {
@ -42,7 +43,7 @@ class BackupCardsWidget(
createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)),
createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP))
)
leapfrogWidget.fold { animator.start() }
leapfrogWidget.fold(animate) { animator.start() }
}
fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) {
@ -70,7 +71,7 @@ class BackupCardsWidget(
)
leapfrogWidget.fold {
animator.doOnEnd {
leapfrogWidget.initViews()
// leapfrogWidget.initViews()
leapfrogWidget.unfold()
}
animator.start()
@ -98,26 +99,26 @@ class BackupCardsWidget(
private fun createWelcomeProperties(cardType: BackupCardType): CardProperties {
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 440f,
yTranslation = 0f,
rotation = 75f,
elevation = 2f,
scale = .9f,
)
xTranslation = 440f * deviceScaleFactor,
yTranslation = 70f,
rotation = 75f,
elevation = 2f,
scale = .85f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 10f,
yTranslation = -100f,
rotation = 100f,
elevation = 1f,
scale = .9f,
)
xTranslation = 10f * deviceScaleFactor,
yTranslation = -30f,
rotation = 100f,
elevation = 1f,
scale = .85f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = -440f,
yTranslation = -100f,
rotation = 70f,
elevation = 0f,
scale = .9f,
)
xTranslation = -440f * deviceScaleFactor,
yTranslation = -30f,
rotation = 70f,
elevation = 0f,
scale = .85f * deviceScaleFactor,
)
}
}
@ -125,24 +126,24 @@ class BackupCardsWidget(
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
yTranslation = 30f,
rotation = 5f,
elevation = 2f,
scale = 1f,
scale = 1f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = -80f,
yTranslation = -50f,
rotation = -5f,
elevation = 1f,
scale = 0.9f,
scale = 0.9f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = 10f,
yTranslation = -170f,
yTranslation = -120f,
rotation = -20f,
elevation = 0f,
scale = 0.8f,
scale = 0.8f * deviceScaleFactor,
)
}
}
@ -154,21 +155,21 @@ class BackupCardsWidget(
yTranslation = 0f,
rotation = 0f,
elevation = 2f,
scale = 1f,
scale = 1f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 1f,
scale = 0.9f,
scale = 0.9f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 0f,
scale = 0.8f,
scale = 0.8f * deviceScaleFactor,
)
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -22,6 +23,8 @@ import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.states.ButtonState
@ -32,6 +35,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -164,9 +168,12 @@ private fun sendTransaction(
)
)
}
val sendResult = try {
(walletManager as TransactionSender).send(txData, signer)
if (walletManager.isDemoWallet()) {
DemoTransactionSender(walletManager).send(txData, signer)
} else {
(walletManager as TransactionSender).send(txData, signer)
}
} catch (ex: Exception) {
FirebaseCrashlytics.getInstance().recordException(ex)
delay(DELAY_SDK_DIALOG_CLOSE)
@ -238,6 +245,12 @@ private fun sendTransaction(
message.contains("Target account is not created. To create account send 1+ XLM.") -> {
dispatch(SendAction.SendError(TapError.XmlError.AssetAccountNotCreated))
}
message.contains(DemoTransactionSender.ID) -> {
store.dispatchDialogShow(AppDialog.SimpleOkDialogRes(
R.string.common_done,
R.string.alert_demo_tx_send
))
}
else -> {
(sendResult.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandlers?.logCardSdkError(

View file

@ -0,0 +1,38 @@
package com.tangem.tap.features.shop.redux
import android.content.Intent
import com.tangem.tap.common.shop.GooglePayService
import com.tangem.tap.common.shop.data.ProductType
import com.tangem.tap.common.shop.data.TangemProduct
import org.rekotlin.Action
sealed class ShopAction : Action {
object LoadProducts : ShopAction() {
data class Success(val products: List<TangemProduct>) : ShopAction()
}
data class ApplyPromoCode(val promoCode: String) : ShopAction() {
data class Success(val promoCode: String?, val products: List<TangemProduct>) : ShopAction()
object InvalidPromoCode : ShopAction()
}
object BuyWithGooglePay : ShopAction() {
object UserCancelled : ShopAction()
data class HandleGooglePayResponse(val resultCode: Int, val data: Intent?) : ShopAction()
data class Failure(val exception: Throwable) : ShopAction()
object Success : ShopAction()
}
object StartWebCheckout : ShopAction()
data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction() {
object Success : ShopAction()
object Failure : ShopAction()
}
data class SelectProduct(val productType: ProductType) : ShopAction()
object ResetState : ShopAction()
}

View file

@ -0,0 +1,116 @@
package com.tangem.tap.features.shop.redux
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.scope
import com.tangem.tap.shopService
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
class ShopMiddleware {
val shopMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handle(action)
next(action)
}
}
}
}
private fun handle(action: Action) {
val shopState = store.state.shopState
if (action is NavigationAction.NavigateTo && action.screen == AppScreen.Shop) {
store.dispatch(ShopAction.LoadProducts)
}
if (action !is ShopAction) return
when (action) {
is ShopAction.ApplyPromoCode -> {
scope.launch {
if (action.promoCode.isBlank() && shopState.promoCode == null) {
store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode)
return@launch
}
val result = shopService.applyPromoCode(action.promoCode)
result.onSuccess { products ->
store.dispatchOnMain(
ShopAction.ApplyPromoCode.Success(
promoCode = products.first { it.type == shopState.selectedProduct }.appliedDiscount,
products = products
)
)
}
result.onFailure { store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode) }
}
}
ShopAction.BuyWithGooglePay -> {
shopService.buyWithGooglePay(shopState.selectedProduct)
// shopService.subscribeToGooglePayResult(productType = shopState.selectedProduct) { result ->
// result.onSuccess {
// store.dispatch(ShopAction.BuyWithGooglePay.Success)
// }
// result.onFailure { error ->
// if (error is TangemSdkError.UserCancelled) {
// store.dispatch(ShopAction.BuyWithGooglePay.UserCancelled)
// } else {
// store.dispatch(ShopAction.BuyWithGooglePay.Failure(error))
// }
// }
// }
}
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse -> {
scope.launch {
val result = shopService.handleGooglePayResult(
action.resultCode,
action.data,
shopState.selectedProduct
)
result.onSuccess {
store.dispatchOnMain(ShopAction.BuyWithGooglePay.Success)
}
result.onFailure {
store.dispatchOnMain(ShopAction.BuyWithGooglePay.Failure(it))
}
}
}
ShopAction.LoadProducts -> {
scope.launch {
val result = shopService.getProducts()
result.onSuccess {
store.dispatchOnMain(ShopAction.LoadProducts.Success(it))
}
}
}
is ShopAction.CheckIfGooglePayAvailable -> {
scope.launch {
val isAvailable =
shopService.checkIfGooglePayAvailable(action.googlePayService).getOrNull()
?: false
val newAction = if (isAvailable) {
ShopAction.CheckIfGooglePayAvailable.Success
} else {
ShopAction.CheckIfGooglePayAvailable.Failure
}
store.dispatchOnMain(newAction)
}
}
ShopAction.StartWebCheckout -> {
store.dispatchOpenUrl(shopService.getCheckoutUrl(shopState.selectedProduct))
}
}
}

View file

@ -0,0 +1,68 @@
package com.tangem.tap.features.shop.redux
import org.rekotlin.Action
class ShopReducer {
companion object {
fun reduce(action: Action, state: ShopState): ShopState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, state: ShopState): ShopState {
if (action !is ShopAction) return state
return when (action) {
is ShopAction.ApplyPromoCode -> state.copy(
promoCode = action.promoCode,
promoCodeLoading = true
)
ShopAction.BuyWithGooglePay -> state
ShopAction.LoadProducts -> state
is ShopAction.LoadProducts.Success -> {
state.copy(
availableProducts = action.products,
)
}
ShopAction.StartWebCheckout -> state
ShopAction.ApplyPromoCode.InvalidPromoCode -> state.copy(
promoCode = null, promoCodeLoading = false
)
is ShopAction.ApplyPromoCode.Success -> {
state.copy(
promoCode = action.promoCode,
availableProducts = action.products,
promoCodeLoading = false
)
}
is ShopAction.SelectProduct -> {
state.copy(
selectedProduct = action.productType,
)
}
is ShopAction.CheckIfGooglePayAvailable -> {
state
}
ShopAction.CheckIfGooglePayAvailable.Failure -> {
state.copy(isGooglePayAvailable = false)
}
ShopAction.CheckIfGooglePayAvailable.Success -> {
state.copy(isGooglePayAvailable = false) // TODO: change when we add support for GPay
}
is ShopAction.BuyWithGooglePay.Failure -> {
state
}
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse -> {
state
}
ShopAction.BuyWithGooglePay.Success -> {
state
}
ShopAction.BuyWithGooglePay.UserCancelled -> {
state
}
ShopAction.ResetState -> ShopState()
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.shop.redux
import com.tangem.tap.common.shop.data.ProductType
import com.tangem.tap.common.shop.data.TangemProduct
import org.rekotlin.StateType
data class ShopState(
val availableProducts: List<TangemProduct> = emptyList(),
val selectedProduct: ProductType = ProductType.WALLET_3_CARDS,
val promoCode: String? = null,
val promoCodeLoading: Boolean = false,
val isGooglePayAvailable: Boolean = false, // TODO: change when we add support for GPay
) : StateType {
val total: String?
get() = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum?.finalValue
val priceBeforeDiscount: String?
get() {
val totalSum = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum
if (totalSum?.finalValue != totalSum?.beforeDiscount) {
return totalSum?.beforeDiscount
}
return null
}
}

View file

@ -0,0 +1,167 @@
package com.tangem.tap.features.shop.ui
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.View.OnFocusChangeListener
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.activity.OnBackPressedCallback
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.shop.data.ProductType
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_shop.*
import org.rekotlin.StoreSubscriber
class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<ShopState> {
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.shopState == newState.shopState
}.select { it.shopState }
}
storeSubscribersList.add(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(ShopAction.ResetState)
}
})
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupCardsImages()
setupProductSelection()
setupPromoCodeEditText()
toolbar.setNavigationOnClickListener {
requireActivity().onBackPressed()
}
val keyboardObserver = KeyboardObserver(requireActivity())
keyboardObserver.registerListener { isVisible ->
fl_cards.show(!isVisible)
}
}
private fun setupCardsImages() {
imv_second.animate()
.translationY(70f)
.scaleX(0.9f)
.scaleY(0.9f)
.start()
imv_third.animate()
.translationY(140f)
.scaleX(0.8f)
.scaleY(0.8f)
.start()
}
private fun setupProductSelection() {
chip_product_1.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_3_CARDS))
}
chip_product_2.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_2_CARDS))
}
}
private fun setupPromoCodeEditText() {
et_promo_code.setOnEditorActionListener { view, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val imm: InputMethodManager =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
view.clearFocus()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
et_promo_code.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
store.dispatch(ShopAction.ApplyPromoCode(et_promo_code.text.toString()))
}
}
}
override fun newState(state: ShopState) {
if (activity == null) return
animateProductSelection(state.selectedProduct)
handlePriceState(state)
handlePromoCodeState(state)
handleButtonsState(state)
}
private fun animateProductSelection(selectedProduct: ProductType) {
val show = when (selectedProduct) {
ProductType.WALLET_2_CARDS -> false
ProductType.WALLET_3_CARDS -> true
}
showOrHideThirdCardWithAnimation(show)
}
private fun showOrHideThirdCardWithAnimation(show: Boolean) {
val translationY = if (show) 140f else 0f
if (show) imv_third.show()
imv_third.animate()
.translationY(translationY)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
imv_third?.show(show)
}
})
}
private fun handlePriceState(state: ShopState) {
tv_total.text = state.total
tv_total_before_discount.text = state.priceBeforeDiscount
pb_price.show(state.total == null)
}
private fun handlePromoCodeState(state: ShopState) {
if (state.promoCode == null && !et_promo_code.hasFocus()) {
et_promo_code.setText("")
}
pb_promo_code.show(state.promoCodeLoading)
}
private fun handleButtonsState(state: ShopState) {
btn_pay_google_pay.show(state.isGooglePayAvailable)
btn_alternative_payment.show(state.isGooglePayAvailable)
btn_main_action.show(!state.isGooglePayAvailable)
if (state.total != null) {
btn_alternative_payment.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) }
btn_main_action.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) }
btn_pay_google_pay.setOnClickListener { store.dispatch(ShopAction.BuyWithGooglePay) }
}
}
override fun handleOnBackPressed() {
store.dispatch(ShopAction.ResetState)
super.handleOnBackPressed()
}
}

View file

@ -11,6 +11,7 @@ import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.tokens.redux.TokensState
@ -96,7 +97,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
val menuItem = menu.findItem(R.id.menu_search)
val searchView: SearchView = menuItem.actionView as SearchView
searchView.queryHint = "Type here to search"
searchView.queryHint = searchView.getString(R.string.add_token_search_hint)
searchView.maxWidth = android.R.attr.width
searchView.inputtedTextAsFlow()
.debounce(400)

View file

@ -6,10 +6,12 @@ import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.Wallet
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.domain.extensions.toSendableAmounts
import java.math.BigDecimal
data class PendingTransaction(
val address: String?,
val amount: String?,
val amount: BigDecimal?,
val amountUi: String?,
val currency: String,
val type: PendingTransactionType
)
@ -38,6 +40,7 @@ fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransact
return PendingTransaction(
if (address == "unknown") null else address,
this.amount.value,
this.amount.value?.toFormattedString(amount.decimals),
this.amount.currencySymbol,
type
@ -61,8 +64,12 @@ fun List<TransactionData>.toPendingTransactionsForToken(token: Token, walletAddr
return this.mapNotNull { it.toPendingTransactionForToken(token, walletAddress) }
}
fun Wallet.getPendingTransactions(): List<PendingTransaction> {
return recentTransactions.toPendingTransactions(address)
fun Wallet.getPendingTransactions(type: PendingTransactionType? = null): List<PendingTransaction> {
val txs = recentTransactions.toPendingTransactions(address)
return when(type) {
null -> txs
else -> txs.filter { it.type == type }
}
}
fun Wallet.hasPendingTransactions(): Boolean {

View file

@ -13,6 +13,10 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
@ -22,6 +26,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class MultiWalletMiddleware {
fun handle(
@ -34,6 +39,9 @@ class MultiWalletMiddleware {
is WalletAction.MultiWallet.AddWalletManagers -> {
globalState.feedbackManager?.infoHolder?.setWalletsInfo(action.walletManagers)
action.walletManagers.forEach { checkForRentWarning(it) }
if (globalState.scanResponse?.isDemoCard() == true) {
addDummyBalances(action.walletManagers)
}
}
is WalletAction.MultiWallet.SelectWallet -> {
if (action.walletData != null) {
@ -99,28 +107,25 @@ class MultiWalletMiddleware {
scope.launch {
walletManagers.map { walletManager ->
async(Dispatchers.IO) {
try {
walletManager.update()
val wallet = walletManager.wallet
val coinAmount = wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
scope.launch(Dispatchers.Main) {
if (walletState?.getWalletData(wallet.blockchain) == null) {
store.dispatch(
WalletAction.MultiWallet.AddWalletManagers(
listOfNotNull(walletManager)
)
walletManager.safeUpdate()
val wallet = walletManager.wallet
val coinAmount = wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
scope.launch(Dispatchers.Main) {
if (walletState?.getWalletData(wallet.blockchain) == null) {
store.dispatch(
WalletAction.MultiWallet.AddWalletManagers(
listOfNotNull(walletManager)
)
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
wallet.blockchain
)
)
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
wallet.blockchain
)
store.dispatch(WalletAction.LoadWallet.Success(wallet))
}
)
store.dispatch(WalletAction.LoadWallet.Success(wallet))
}
}
} catch (exception: Exception) {
}
}
}
@ -167,6 +172,14 @@ class MultiWalletMiddleware {
}
}
private fun addDummyBalances(walletManagers: List<WalletManager>) {
walletManagers.forEach {
if (it.wallet.fundsAvailable(AmountType.Coin) == BigDecimal.ZERO) {
DemoHelper.injectDemoBalance(it)
}
}
}
private fun addToken(token: Token, walletState: WalletState?, globalState: GlobalState?) {
val scanResponse = globalState?.scanResponse ?: return
@ -206,13 +219,25 @@ class MultiWalletMiddleware {
scope.launch {
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is Result.Success -> {
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean = balance >= rentExempt
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs = walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
val rentExempt = result.data
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
if (!show) return@launch
val currency = walletManager.wallet.blockchain.currency
val minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency")
val rentExempt = ("${result.data.stripZeroPlainString()} $currency")
store.dispatchOnMain(WalletAction.SetWalletRent(
blockchain = walletManager.wallet.blockchain,
minRent = minRent,
rentExempt = rentExempt
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
))
}
is Result.Failure -> {}

View file

@ -3,10 +3,12 @@ package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.topup.TopUpManager
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.Currency
@ -18,7 +20,9 @@ import timber.log.Timber
class TradeCryptoMiddleware {
fun handle(action: WalletAction.TradeCryptoAction) {
fun handle(state: () -> AppState?, action: WalletAction.TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
when (action) {
is WalletAction.TradeCryptoAction.Buy -> startExchange(action)
is WalletAction.TradeCryptoAction.Sell -> startExchange(action)

View file

@ -3,13 +3,15 @@ package com.tangem.tap.features.wallet.redux.middlewares
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.*
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
@ -18,10 +20,14 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
@ -37,18 +43,20 @@ class WalletMiddleware {
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleAction(action, dispatch)
handleAction(state, action, dispatch)
next(action)
}
}
}
private fun handleAction(action: Action, dispatch: DispatchFunction) {
private fun handleAction(state: () -> AppState?, action: Action, dispatch: DispatchFunction) {
if (DemoHelper.tryHandle(state, action)) return
val globalState = store.state.globalState
val walletState = store.state.walletState
when (action) {
is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(action)
is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action)
is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState)
is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState, globalState)
is WalletAction.LoadWallet -> {

View file

@ -17,6 +17,7 @@ import com.tangem.tap.domain.extensions.hasSignedHashes
import com.tangem.tap.domain.extensions.remainingSignatures
import com.tangem.tap.domain.isMultiwalletAllowed
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.preferencesStorage
@ -99,6 +100,9 @@ class WarningsMiddleware {
addWarningMessage(WarningMessagesManager.onlineVerificationFailed())
}
}
if (scanResponse.isDemoCard()){
addWarningMessage(WarningMessagesManager.demoCardWarning())
}
setWarningMessages()
}
}

View file

@ -23,8 +23,6 @@ import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_wallet_details.*
import kotlinx.android.synthetic.main.item_currency_wallet.view.*
import kotlinx.android.synthetic.main.item_popular_token.view.*
import kotlinx.android.synthetic.main.layout_balance_error.*
import kotlinx.android.synthetic.main.layout_balance_wallet_details.*
import kotlinx.android.synthetic.main.layout_wallet_details.*
@ -197,7 +195,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
requireContext()))
}
iv_qr_code.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode())
tv_recieve_message.text = getQRReceiveMessage(tv_recieve_message.context, state.currency)
tv_receive_message.text = getQRReceiveMessage(tv_receive_message.context, state.currency)
}
}

View file

@ -62,7 +62,7 @@ class PendingTransactionsAdapter
}
view.tv_pending_transaction.text = view.context.getString(transactionDescriptionRes)
transaction.amount?.let { view.tv_pending_transaction_amount.text = "$it " }
transaction.amountUi?.let { view.tv_pending_transaction_amount.text = "$it " }
view.tv_pending_transaction_currency.text = "${transaction.currency}"
if (transaction.address != null) {

View file

@ -0,0 +1,35 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class SimpleOkDialog {
companion object {
fun create(dialog: AppDialog.SimpleOkDialog, context: Context): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(dialog.header)
setMessage(dialog.message)
setPositiveButton(R.string.common_ok) { _, _ -> }
setOnDismissListener { store.dispatchDialogHide() }
}.create()
}
fun create(dialog: AppDialog.SimpleOkDialogRes, context: Context): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(context.getString(dialog.headerId))
setMessage(dialog.messageId)
setPositiveButton(R.string.common_ok) { _, _ -> }
setOnDismissListener { store.dispatchDialogHide() }
}.create()
}
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 24% opacity -->
<item android:color="@color/chipBlack" android:state_enabled="true" android:state_selected="true" />
<item android:color="@color/chipBlack" android:state_checked="true" android:state_enabled="true" />
<item android:color="@color/backgroundGray" android:state_enabled="true" />
<item android:color="@color/backgroundGray" />
</selector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 24% opacity -->
<item android:color="@color/backgroundGray" android:state_enabled="true" android:state_selected="true" />
<item android:color="@color/backgroundGray" android:state_checked="true" android:state_enabled="true" />
<item android:color="@color/chipBlack" android:state_enabled="true" />
<item android:color="@color/chipBlack" />
</selector>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 837 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

View file

@ -0,0 +1,54 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="103dp"
android:height="17dp"
android:viewportWidth="103.0"
android:viewportHeight="17.0">
<path
android:pathData="M0.148,2.976L3.914,2.976C4.446,2.976 4.938,3.093 5.391,3.326C5.844,3.559 6.205,3.881 6.476,4.292C6.747,4.703 6.882,5.155 6.882,5.65C6.882,6.145 6.758,6.574 6.511,6.938C6.264,7.302 5.939,7.577 5.538,7.764L5.538,7.848C6.042,8.025 6.45,8.319 6.763,8.73C7.076,9.141 7.232,9.621 7.232,10.172C7.232,10.723 7.09,11.213 6.805,11.642C6.52,12.071 6.138,12.405 5.657,12.643C5.176,12.881 4.651,13 4.082,13L0.148,13L0.148,2.976ZM3.844,7.176C4.292,7.176 4.654,7.036 4.929,6.756C5.204,6.476 5.342,6.154 5.342,5.79C5.342,5.426 5.209,5.106 4.943,4.831C4.677,4.556 4.329,4.418 3.9,4.418L1.716,4.418L1.716,7.176L3.844,7.176ZM4.082,11.544C4.558,11.544 4.938,11.395 5.223,11.096C5.508,10.797 5.65,10.452 5.65,10.06C5.65,9.659 5.503,9.311 5.209,9.017C4.915,8.723 4.521,8.576 4.026,8.576L1.716,8.576L1.716,11.544L4.082,11.544ZM9.461,12.447C9.008,11.929 8.782,11.208 8.782,10.284L8.782,5.86L10.322,5.86L10.322,10.074C10.322,10.653 10.46,11.087 10.735,11.376C11.01,11.665 11.372,11.81 11.82,11.81C12.184,11.81 12.506,11.714 12.786,11.523C13.066,11.332 13.281,11.077 13.43,10.76C13.579,10.443 13.654,10.102 13.654,9.738L13.654,5.86L15.194,5.86L15.194,13L13.738,13L13.738,12.076L13.654,12.076C13.458,12.412 13.155,12.687 12.744,12.902C12.333,13.117 11.899,13.224 11.442,13.224C10.574,13.224 9.914,12.965 9.461,12.447ZM19.32,12.608L16.352,5.86L18.074,5.86L20.09,10.718L20.146,10.718L22.106,5.86L23.8,5.86L19.39,16.024L17.766,16.024L19.32,12.608ZM27.586,5.86L29.252,5.86L30.694,10.97L30.75,10.97L32.36,5.86L33.942,5.86L35.538,10.97L35.594,10.97L37.036,5.86L38.674,5.86L36.392,13L34.768,13L33.13,7.876L33.088,7.876L31.464,13L29.868,13L27.586,5.86ZM39.965,4.523C39.764,4.322 39.664,4.077 39.664,3.788C39.664,3.499 39.764,3.254 39.965,3.053C40.166,2.852 40.411,2.752 40.7,2.752C40.989,2.752 41.234,2.852 41.435,3.053C41.636,3.254 41.736,3.499 41.736,3.788C41.736,4.077 41.636,4.322 41.435,4.523C41.234,4.724 40.989,4.824 40.7,4.824C40.411,4.824 40.166,4.724 39.965,4.523ZM39.93,5.86L41.47,5.86L41.47,13L39.93,13L39.93,5.86ZM45.498,12.958C45.218,12.855 44.989,12.72 44.812,12.552C44.411,12.151 44.21,11.605 44.21,10.914L44.21,7.218L42.964,7.218L42.964,5.86L44.21,5.86L44.21,3.844L45.75,3.844L45.75,5.86L47.486,5.86L47.486,7.218L45.75,7.218L45.75,10.578C45.75,10.961 45.825,11.231 45.974,11.39C46.114,11.577 46.357,11.67 46.702,11.67C46.861,11.67 47.001,11.649 47.122,11.607C47.243,11.565 47.374,11.497 47.514,11.404L47.514,12.902C47.206,13.042 46.833,13.112 46.394,13.112C46.077,13.112 45.778,13.061 45.498,12.958ZM49.176,2.976L50.716,2.976L50.716,5.706L50.646,6.798L50.716,6.798C50.921,6.462 51.227,6.184 51.633,5.965C52.039,5.746 52.475,5.636 52.942,5.636C53.81,5.636 54.473,5.89 54.93,6.399C55.387,6.908 55.616,7.601 55.616,8.478L55.616,13L54.076,13L54.076,8.688C54.076,8.147 53.934,7.741 53.649,7.47C53.364,7.199 52.993,7.064 52.536,7.064C52.191,7.064 51.88,7.162 51.605,7.358C51.33,7.554 51.113,7.813 50.954,8.135C50.795,8.457 50.716,8.8 50.716,9.164L50.716,13L49.176,13L49.176,2.976Z"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#FFFFFF"
android:strokeWidth="1"/>
<path
android:pathData="M81.526,2.635L81.526,6.718L84.044,6.718C84.644,6.718 85.14,6.516 85.532,6.113C85.935,5.711 86.137,5.231 86.137,4.676C86.137,4.132 85.935,3.658 85.532,3.254C85.14,2.841 84.644,2.634 84.044,2.634L81.526,2.634L81.526,2.635ZM81.526,8.155L81.526,12.891L80.022,12.891L80.022,1.198L84.011,1.198C85.025,1.198 85.885,1.535 86.594,2.21C87.314,2.885 87.674,3.707 87.674,4.676C87.674,5.667 87.314,6.495 86.594,7.158C85.897,7.823 85.035,8.154 84.011,8.154L81.526,8.154L81.526,8.155Z"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#FFFFFF"
android:strokeWidth="1"/>
<path
android:pathData="M89.194,10.442C89.194,10.834 89.36,11.16 89.693,11.422C90.025,11.683 90.415,11.813 90.861,11.813C91.494,11.813 92.057,11.579 92.553,11.112C93.05,10.643 93.297,10.093 93.297,9.463C92.828,9.092 92.174,8.907 91.335,8.907C90.724,8.907 90.215,9.055 89.807,9.349C89.398,9.643 89.194,10.006 89.194,10.442M91.14,4.627C92.252,4.627 93.129,4.924 93.773,5.518C94.415,6.111 94.737,6.925 94.737,7.959L94.737,12.891L93.298,12.891L93.298,11.781L93.233,11.781C92.611,12.695 91.783,13.153 90.747,13.153C89.865,13.153 89.126,12.891 88.532,12.369C87.938,11.846 87.641,11.193 87.641,10.409C87.641,9.581 87.954,8.923 88.581,8.433C89.208,7.943 90.044,7.698 91.09,7.698C91.983,7.698 92.72,7.861 93.297,8.188L93.297,7.844C93.297,7.322 93.09,6.878 92.676,6.513C92.261,6.149 91.777,5.967 91.221,5.967C90.381,5.967 89.717,6.32 89.226,7.029L87.902,6.195C88.632,5.15 89.711,4.627 91.14,4.627"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#FFFFFF"
android:strokeWidth="1"/>
<path
android:pathData="M102.993,4.889l-5.02,11.531l-1.553,0l1.864,-4.035l-3.303,-7.496l1.635,0l2.387,5.749l0.033,0l2.322,-5.749z"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#FFFFFF"
android:strokeWidth="1"/>
<path
android:pathData="M75.448,7.134C75.448,6.661 75.408,6.205 75.332,5.768L68.988,5.768L68.988,8.356L72.622,8.356C72.466,9.199 71.994,9.917 71.278,10.398L71.278,12.079L73.447,12.079C74.716,10.908 75.448,9.179 75.448,7.134"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#4285F4"
android:strokeWidth="1"/>
<path
android:pathData="M68.988,13.701C70.804,13.701 72.332,13.105 73.447,12.079L71.278,10.398C70.675,10.804 69.897,11.041 68.988,11.041C67.234,11.041 65.744,9.859 65.212,8.267L62.978,8.267L62.978,9.998C64.085,12.193 66.36,13.701 68.988,13.701"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#34A853"
android:strokeWidth="1"/>
<path
android:pathData="M65.212,8.267C65.076,7.861 65.001,7.428 65.001,6.981C65.001,6.534 65.076,6.101 65.212,5.695L65.212,3.964L62.978,3.964C62.52,4.871 62.261,5.896 62.261,6.981C62.261,8.066 62.52,9.091 62.978,9.998L65.212,8.267Z"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#FABB05"
android:strokeWidth="1"/>
<path
android:pathData="M68.988,2.921C69.98,2.921 70.868,3.262 71.569,3.929L71.569,3.93L73.489,2.012C72.323,0.928 70.803,0.261 68.988,0.261C66.36,0.261 64.085,1.769 62.978,3.964L65.212,5.695C65.744,4.103 67.234,2.921 68.988,2.921"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#E94235"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:drawable="@drawable/googlepay_button_background_image" />
</selector>

View file

@ -0,0 +1,48 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="41dp"
android:height="17dp"
android:viewportWidth="41.0"
android:viewportHeight="17.0">
<path
android:pathData="M19.526,2.635L19.526,6.718L22.044,6.718C22.644,6.718 23.14,6.516 23.532,6.113C23.935,5.711 24.137,5.231 24.137,4.676C24.137,4.132 23.935,3.658 23.532,3.254C23.14,2.841 22.644,2.634 22.044,2.634L19.526,2.634L19.526,2.635ZM19.526,8.155L19.526,12.891L18.022,12.891L18.022,1.198L22.011,1.198C23.025,1.198 23.885,1.535 24.594,2.21C25.314,2.885 25.674,3.707 25.674,4.676C25.674,5.667 25.314,6.495 24.594,7.158C23.897,7.823 23.035,8.154 22.011,8.154L19.526,8.154L19.526,8.155Z"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#FFFFFF"
android:strokeWidth="1"/>
<path
android:pathData="M27.194,10.442C27.194,10.834 27.36,11.16 27.693,11.422C28.025,11.683 28.415,11.813 28.861,11.813C29.494,11.813 30.057,11.579 30.553,11.112C31.05,10.643 31.297,10.093 31.297,9.463C30.828,9.092 30.174,8.907 29.335,8.907C28.724,8.907 28.215,9.055 27.807,9.349C27.398,9.643 27.194,10.006 27.194,10.442M29.14,4.627C30.252,4.627 31.129,4.924 31.773,5.518C32.415,6.111 32.737,6.925 32.737,7.959L32.737,12.891L31.298,12.891L31.298,11.781L31.233,11.781C30.611,12.695 29.783,13.153 28.747,13.153C27.865,13.153 27.126,12.891 26.532,12.369C25.938,11.846 25.641,11.193 25.641,10.409C25.641,9.581 25.954,8.923 26.581,8.433C27.208,7.943 28.044,7.698 29.09,7.698C29.983,7.698 30.72,7.861 31.297,8.188L31.297,7.844C31.297,7.322 31.09,6.878 30.676,6.513C30.261,6.149 29.777,5.967 29.221,5.967C28.381,5.967 27.717,6.32 27.226,7.029L25.902,6.195C26.632,5.15 27.711,4.627 29.14,4.627"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#FFFFFF"
android:strokeWidth="1"/>
<path
android:pathData="M40.993,4.889l-5.02,11.531l-1.553,0l1.864,-4.035l-3.303,-7.496l1.635,0l2.387,5.749l0.033,0l2.322,-5.749z"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#FFFFFF"
android:strokeWidth="1"/>
<path
android:pathData="M13.448,7.134C13.448,6.661 13.408,6.205 13.332,5.768L6.988,5.768L6.988,8.356L10.622,8.356C10.466,9.199 9.994,9.917 9.278,10.398L9.278,12.079L11.447,12.079C12.716,10.908 13.448,9.179 13.448,7.134"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#4285F4"
android:strokeWidth="1"/>
<path
android:pathData="M6.988,13.701C8.804,13.701 10.332,13.105 11.447,12.079L9.278,10.398C8.675,10.804 7.897,11.041 6.988,11.041C5.234,11.041 3.744,9.859 3.212,8.267L0.978,8.267L0.978,9.998C2.085,12.193 4.36,13.701 6.988,13.701"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#34A853"
android:strokeWidth="1"/>
<path
android:pathData="M3.212,8.267C3.076,7.861 3.001,7.428 3.001,6.981C3.001,6.534 3.076,6.101 3.212,5.695L3.212,3.964L0.978,3.964C0.52,4.871 0.261,5.896 0.261,6.981C0.261,8.066 0.52,9.091 0.978,9.998L3.212,8.267Z"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#FABB05"
android:strokeWidth="1"/>
<path
android:pathData="M6.988,2.921C7.98,2.921 8.868,3.262 9.569,3.929L9.569,3.93L11.489,2.012C10.323,0.928 8.803,0.261 6.988,0.261C4.36,0.261 2.085,1.769 0.978,3.964L3.212,5.695C3.744,4.103 5.234,2.921 6.988,2.921"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#E94235"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:drawable="@drawable/googlepay_button_no_shadow_background_image" />
</selector>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:state_enabled="false">
<shape
android:shape="rectangle" >
<corners android:radius="4dp"/>
<solid android:color="#7FFFFFFF"/>
</shape>
</item>
<item android:drawable="@android:color/transparent" />
</selector>

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="@color/iconGray" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector android:autoMirrored="true" android:height="20dp"
android:viewportHeight="20" android:viewportWidth="20"
android:width="20dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0h20v20h-20z"/>
<path android:fillColor="#00000000"
android:pathData="M17.2412,14.1916L17.2412,11.7284C16.8163,11.6812 16.4151,11.516 16.0847,11.1855C15.3295,10.4303 15.3531,9.2266 16.0847,8.495C16.4151,8.1646 16.8163,7.9994 17.2412,7.9522L17.2412,5.4889C17.2412,5.1585 16.958,4.8753 16.6275,4.8753L3.4197,4.8281C3.0893,4.8281 2.806,5.1113 2.806,5.4417L2.806,7.905C3.2309,7.9522 3.6793,8.1174 4.0097,8.4478C4.7649,9.203 4.7413,10.4067 4.0097,11.1383C3.6793,11.4688 3.2545,11.6576 2.806,11.6812L2.806,14.1444C2.806,14.4748 3.0893,14.7581 3.4197,14.7581L16.6275,14.7581C16.958,14.8053 17.2412,14.522 17.2412,14.1916Z"
android:strokeColor="#090E13" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.1"/>
</group>
</vector>

View file

@ -0,0 +1,11 @@
<vector android:autoMirrored="true" android:height="20dp"
android:viewportHeight="20" android:viewportWidth="20"
android:width="20dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M2.4,6H14.4V17.2H2.4V6ZM17.6,2.8L14.4,6H2.4L6.5852,2.8H17.6Z"
android:strokeColor="#000000" android:strokeLineJoin="round" android:strokeWidth="1.1"/>
<path android:fillColor="#00000000"
android:pathData="M8.4,6L12,2.8M17.6,13.2L14.4,17.2V6L17.6,2.8V13.2ZM6.4,8.4H10.4H6.4Z"
android:strokeColor="#000000" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="1.1"/>
</vector>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false">
<shape android:shape="line">
<stroke android:width="1dp" android:color="@color/darkGray6"/>
</shape>
</item>
</selector>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/backgroundGray" />
<corners android:radius="4dp" />
</shape>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:clickable="true"
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="48sp"
android:background="@drawable/googlepay_button_no_shadow_background"
android:padding="2sp"
android:contentDescription="@string/buy_with_googlepay_button_content_description">
<LinearLayout
android:duplicateParentState="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:weightSum="2"
android:gravity="center_vertical"
android:orientation="vertical">
<ImageView
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scaleType="fitCenter"
android:duplicateParentState="true"
android:src="@drawable/buy_with_googlepay_button_content"/>
</LinearLayout>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:duplicateParentState="true"
android:src="@drawable/googlepay_button_overlay"/>
</RelativeLayout>

View file

@ -24,7 +24,7 @@
app:layout_constraintTop_toBottomOf="@+id/pseudo_toolbar" />
<TextView
android:id="@+id/tv_recieve_message"
android:id="@+id/tv_receive_message"
style="@style/TextViewOnboarding.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -21,7 +21,7 @@
android:layout_height="?attr/actionBarSize"
app:menu="@menu/popular_tokens"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="Add tokens" />
app:title="@string/add_tokens_title" />
</com.google.android.material.appbar.AppBarLayout>
@ -51,7 +51,7 @@
android:layout_marginBottom="16dp"
android:enabled="false"
android:gravity="center"
android:text="Save changes"
android:text="@string/common_save_changes"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

View file

@ -133,32 +133,39 @@
app:layout_constraintTop_toBottomOf="@id/tv_issuer"
tools:text="48 hashes" />
<TextView
android:id="@+id/tv_security"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes_title"
tools:text="Long Tap" />
<TextView
android:id="@+id/tv_security_title"
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_manage_security"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_manage_security"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes" />
app:layout_constraintTop_toBottomOf="@+id/tv_signed_hashes_title">
<TextView
android:id="@+id/tv_security_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/details_row_title_manage_security"
android:textColor="@color/darkGray6"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_security"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawablePadding="15dp"
android:gravity="end"
android:maxLines="2"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
tools:text="Long Tap" />
</androidx.appcompat.widget.LinearLayoutCompat>
<TextView
android:id="@+id/tv_create_backup"
@ -173,7 +180,7 @@
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_security_title" />
app:layout_constraintTop_toBottomOf="@id/ll_manage_security" />
<TextView
android:id="@+id/tv_reset_to_factory"

Some files were not shown because too many files have changed in this diff Show more