Updated on 2026-08-14
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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@{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ data class NavigationState(
|
|||
|
||||
enum class AppScreen {
|
||||
Home,
|
||||
Shop,
|
||||
Disclaimer,
|
||||
OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther,
|
||||
Wallet, WalletDetails,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.tap.common.shop.data
|
||||
|
||||
data class TotalSum(
|
||||
val finalValue: String? = null,
|
||||
val beforeDiscount: String? = null,
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
153
app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
12
app/src/main/java/com/tangem/tap/features/demo/Extentions.kt
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 don’t 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 = "Can’t 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 = "Can’t 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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 -> {}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
app/src/main/res/color/selector_chip_shop.xml
Normal 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>
|
||||
11
app/src/main/res/color/selector_chip_shop_text.xml
Normal 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>
|
||||
60
app/src/main/res/drawable-de/buy_with_googlepay_button_content.xml
Executable file
54
app/src/main/res/drawable-fr/buy_with_googlepay_button_content.xml
Executable file
|
Before Width: | Height: | Size: 119 KiB |
BIN
app/src/main/res/drawable-hdpi/card_placeholder_wallet.9.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
54
app/src/main/res/drawable-it/buy_with_googlepay_button_content.xml
Executable file
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 54 KiB |
BIN
app/src/main/res/drawable-mdpi/card_placeholder_wallet.9.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
54
app/src/main/res/drawable-ru/buy_with_googlepay_button_content.xml
Executable file
|
Before Width: | Height: | Size: 236 KiB |
BIN
app/src/main/res/drawable-xhdpi/card_placeholder_wallet.9.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
app/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png
Executable file
|
After Width: | Height: | Size: 963 B |
|
After Width: | Height: | Size: 263 B |
|
Before Width: | Height: | Size: 499 KiB |
BIN
app/src/main/res/drawable-xxhdpi/card_placeholder_wallet.9.png
Normal file
|
After Width: | Height: | Size: 509 KiB |
BIN
app/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png
Executable file
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 679 B |
|
Before Width: | Height: | Size: 837 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/card_placeholder_wallet.9.png
Normal file
|
After Width: | Height: | Size: 808 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png
Executable file
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 819 B |
54
app/src/main/res/drawable/buy_with_googlepay_button_content.xml
Executable 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>
|
||||
6
app/src/main/res/drawable/googlepay_button_background.xml
Executable 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>
|
||||
|
||||
48
app/src/main/res/drawable/googlepay_button_content.xml
Executable 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>
|
||||
6
app/src/main/res/drawable/googlepay_button_no_shadow_background.xml
Executable 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>
|
||||
|
||||
12
app/src/main/res/drawable/googlepay_button_overlay.xml
Executable 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>
|
||||
|
||||
5
app/src/main/res/drawable/ic_clear_24.xml
Normal 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>
|
||||
11
app/src/main/res/drawable/ic_promo_code.xml
Normal 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>
|
||||
11
app/src/main/res/drawable/ic_shipping.xml
Normal 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>
|
||||
8
app/src/main/res/drawable/shape_line.xml
Normal 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>
|
||||
8
app/src/main/res/drawable/shape_rectangle_rounded_4.xml
Normal 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>
|
||||
31
app/src/main/res/layout/buy_with_googlepay_button.xml
Executable 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||