Updated on 2026-08-14

This commit is contained in:
Tangem 2022-02-23 13:22:24 +03:00
commit ebe8e21504
75 changed files with 2567 additions and 26 deletions

View file

@ -38,7 +38,7 @@ android {
}
debug_beta {
initWith release
debuggable false
debuggable true
versionNameSuffix "-beta"
applicationIdSuffix ".debug"
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
@ -129,6 +129,15 @@ dependencies {
// animation
implementation "com.airbnb.android:lottie:3.4.0"
// Shopify
implementation('com.shopify.mobilebuysdk:buy3:12.0.0') {
exclude group: "com.shopify.graphql.support"
exclude module: 'joda-time'
}
// Google Pay
implementation 'com.google.android.gms:play-services-wallet:19.1.0'
testImplementation 'junit:junit:4.13.2'
testImplementation "com.google.truth:truth:1.1.3"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,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)
@ -57,7 +57,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
yTranslationFactor = 25f * deviceScaleFactor,
)
val leapfrog = LeapfrogWidget(fl_cards_container, leapfrogCalculator)
cardsWidget = BackupCardsWidget(leapfrog, deviceScaleFactor) { 200f * deviceScaleFactor }
cardsWidget = WalletCardsWidget(leapfrog, deviceScaleFactor) { 200f * deviceScaleFactor }
startPostponedEnterTransition()
view_pager_backup_info.adapter = BackupInfoAdapter()

View file

@ -10,7 +10,7 @@ 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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,6 +15,8 @@ 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
@ -217,13 +219,25 @@ class MultiWalletMiddleware {
scope.launch {
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is Result.Success -> {
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean = balance >= rentExempt
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs = walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
val rentExempt = result.data
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
if (!show) return@launch
val currency = walletManager.wallet.blockchain.currency
val minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency")
val rentExempt = ("${result.data.stripZeroPlainString()} $currency")
store.dispatchOnMain(WalletAction.SetWalletRent(
blockchain = walletManager.wallet.blockchain,
minRent = minRent,
rentExempt = rentExempt
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
))
}
is Result.Failure -> {}

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 837 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -81,7 +81,7 @@
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_new"
android:src="@drawable/card_placeholder_black"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatImageView
@ -92,7 +92,7 @@
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_new"
android:src="@drawable/card_placeholder_black"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatImageView
@ -103,7 +103,7 @@
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_new"
android:src="@drawable/card_placeholder_black"
android:transitionName="imv_front_card" />
</FrameLayout>

View file

@ -0,0 +1,375 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:clipChildren="false"
android:clipToPadding="false"
android:fitsSystemWindows="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/ThemeOverlay.MyTheme.Toolbar.AccentColorMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_clear_24"
app:title="@string/shop_title"
app:titleCentered="true" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:clickable="true"
android:fitsSystemWindows="true"
android:focusable="true"
android:focusableInTouchMode="true">
<FrameLayout
android:id="@+id/fl_cards"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="36dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="36dp"
android:layout_weight="1"
app:layout_constraintBottom_toTopOf="@+id/tv_header"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/imv_third"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_wallet"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/tv_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/darkGray2" />
<ImageView
android:id="@+id/imv_second"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_wallet"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/tv_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/darkGray4" />
<ImageView
android:id="@+id/imv_first"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder_wallet"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@id/tv_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</FrameLayout>
<TextView
android:id="@+id/tv_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="44dp"
android:layout_marginBottom="14dp"
android:fontFamily="sans-serif-medium"
android:text="@string/shop_one_wallet"
android:textColor="#060606"
android:textSize="24sp"
app:layout_constraintBottom_toTopOf="@+id/chip_group_product"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/fl_cards" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group_product"
android:layout_width="wrap_content"
android:layout_height="42dp"
android:layout_marginBottom="24dp"
android:background="@drawable/shape_rectangle_rounded_100"
android:backgroundTint="@color/backgroundGray"
android:paddingStart="3dp"
android:paddingTop="3dp"
android:paddingEnd="3dp"
app:layout_constraintBottom_toTopOf="@id/v_shipping_discount_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:selectionRequired="true"
app:singleLine="true"
app:singleSelection="true">
<com.google.android.material.chip.Chip
android:id="@+id/chip_product_1"
style="@style/ShopChips"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:checked="true"
android:text="@string/shop_3_cards"
app:chipMinTouchTargetSize="0dp" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_product_2"
style="@style/ShopChips"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:checked="false"
android:text="@string/shop_2_cards"
app:chipMinTouchTargetSize="0dp" />
</com.google.android.material.chip.ChipGroup>
<View
android:id="@+id/v_shipping_discount_background"
android:layout_width="match_parent"
android:layout_height="93dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/shape_rectangle_rounded_4"
app:layout_constraintBottom_toTopOf="@id/v_total_background" />
<ImageView
android:id="@+id/iv_delivery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:src="@drawable/ic_shipping"
app:layout_constraintBottom_toTopOf="@id/v_divider"
app:layout_constraintStart_toStartOf="@id/v_shipping_discount_background"
app:layout_constraintTop_toTopOf="@id/v_shipping_discount_background" />
<TextView
android:id="@+id/tv_delivery_worldwide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:text="@string/shop_shipping"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@id/v_divider"
app:layout_constraintStart_toEndOf="@id/iv_delivery"
app:layout_constraintTop_toTopOf="@id/v_shipping_discount_background" />
<TextView
android:id="@+id/tv_delivery_cost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/shop_free"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@id/v_divider"
app:layout_constraintEnd_toEndOf="@id/v_shipping_discount_background"
app:layout_constraintTop_toTopOf="@id/v_shipping_discount_background" />
<View
android:id="@+id/v_divider"
android:layout_width="0dp"
android:layout_height="0.5dp"
android:background="#BEBEBE"
app:layout_constraintBottom_toBottomOf="@id/v_shipping_discount_background"
app:layout_constraintEnd_toEndOf="@id/v_shipping_discount_background"
app:layout_constraintStart_toStartOf="@id/v_shipping_discount_background"
app:layout_constraintTop_toTopOf="@id/v_shipping_discount_background" />
<ImageView
android:id="@+id/iv_promo_code"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="12dp"
android:src="@drawable/ic_promo_code"
app:layout_constraintBottom_toBottomOf="@id/v_shipping_discount_background"
app:layout_constraintStart_toStartOf="@id/v_shipping_discount_background"
app:layout_constraintTop_toBottomOf="@id/v_divider" />
<EditText
android:id="@+id/et_promo_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="@string/shop_i_have_a_promo_code"
android:imeOptions="actionDone"
android:inputType="textCapCharacters"
android:paddingStart="6dp"
android:paddingEnd="6dp"
android:singleLine="true"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/v_shipping_discount_background"
app:layout_constraintEnd_toEndOf="@id/v_shipping_discount_background"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/iv_promo_code"
app:layout_constraintTop_toBottomOf="@id/v_divider" />
<ProgressBar
android:id="@+id/pb_promo_code"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_marginEnd="16dp"
android:indeterminateTint="@color/darkGray2"
app:layout_constraintBottom_toBottomOf="@id/v_shipping_discount_background"
app:layout_constraintEnd_toEndOf="@id/v_shipping_discount_background"
app:layout_constraintTop_toBottomOf="@id/v_divider" />
<View
android:id="@+id/v_total_background"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="21dp"
android:background="@drawable/shape_rectangle_rounded_4"
app:layout_constraintBottom_toTopOf="@id/ll_buttons" />
<TextView
android:id="@+id/tv_total_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/shop_total"
android:textColor="@color/textBlack"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/v_total_background"
app:layout_constraintStart_toStartOf="@id/v_total_background"
app:layout_constraintTop_toTopOf="@id/v_total_background" />
<TextView
android:id="@+id/tv_total_before_discount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:background="@drawable/shape_line"
android:textColor="@color/darkGray6"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@id/v_total_background"
app:layout_constraintEnd_toStartOf="@id/tv_total"
app:layout_constraintTop_toTopOf="@id/v_total_background"
tools:text="$44.20" />
<TextView
android:id="@+id/tv_total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="6dp"
android:paddingEnd="12dp"
android:textColor="@color/darkGray6"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/v_total_background"
app:layout_constraintEnd_toEndOf="@id/v_total_background"
app:layout_constraintTop_toTopOf="@id/v_total_background"
tools:text="$44.20" />
<ProgressBar
android:id="@+id/pb_price"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginEnd="16dp"
android:indeterminateTint="@color/darkGray2"
app:layout_constraintBottom_toBottomOf="@id/v_total_background"
app:layout_constraintEnd_toEndOf="@id/v_total_background"
app:layout_constraintTop_toTopOf="@id/v_total_background" />
<LinearLayout
android:id="@+id/ll_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent">
<include
android:id="@+id/btn_pay_google_pay"
layout="@layout/buy_with_googlepay_button"
android:layout_width="match_parent"
android:layout_height="48sp"
android:layout_gravity="bottom"
android:layout_marginStart="14dp"
android:layout_marginEnd="14dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_main_action"
style="@style/TapBlackButton"
android:layout_width="match_parent"
android:layout_height="48sp"
android:layout_marginStart="14dp"
android:layout_marginEnd="14dp"
android:layout_marginBottom="70dp"
android:text="@string/shop_buy_now"
android:textAlignment="center"
android:textSize="16sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_alternative_payment"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="match_parent"
android:layout_height="42dp"
android:layout_gravity="bottom"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="20dp"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:text="@string/shop_other_payment_methods"
android:textAllCaps="false"
android:textColor="@color/textBlack"
android:textSize="16sp"
android:visibility="gone" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn_2_cards"
style="@style/TapButton"
android:text="2 cards"
app:layout_constraintEnd_toStartOf="@+id/btn_3_cards"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="16dp"/>
<Button
android:id="@+id/btn_3_cards"
style="@style/TapButton"
android:text="3 cards"
android:layout_margin="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/btn_2_cards"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_promo_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:hint="Promo Code"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:textAlignment="center"/>
<TextView
android:id="@+id/tv_price"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:text="1.00 USD"
app:layout_constraintBottom_toTopOf="@id/tv_total"
android:textAlignment="center"
android:padding="16dp"/>
<TextView
android:id="@+id/tv_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:text="1.00 USD"
app:layout_constraintBottom_toTopOf="@id/pay_gpay"
android:textAlignment="center"
android:padding="16dp"/>
<Button
android:id="@+id/pay_gpay"
style="@style/TapButton"
android:text="GPay"
app:layout_constraintEnd_toStartOf="@+id/pay_web"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_margin="16dp"/>
<Button
android:id="@+id/pay_web"
style="@style/TapButton"
android:text="Pay online"
android:layout_margin="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/pay_gpay"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -29,6 +29,11 @@
<color name="buttonGray">#F4F5F6</color>
<color name="textGray">#DE000000</color>
<color name="textBlack">#060606</color>
<color name="iconGray">#686868</color>
<color name="backgroundGray">#F2F2F2</color>
<color name="chipBlack">#090E13</color>
<color name="twins_dark">#14181D</color>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="googlepay_button_content_description">Google Pay</string>
<string name="buy_with_googlepay_button_content_description">Buy with Google Pay</string>
<string name="donate_with_googlepay_button_content_description">Donate With Google Pay</string>
<string name="pay_with_googlepay_button_content_description">Pay With Google Pay</string>
<string name="subscribe_with_googlepay_button_content_description">Subscribe With Google Pay</string>
<string name="book_with_googlepay_button_content_description">Book With Google Pay</string>
<string name="checkout_with_googlepay_button_content_description">Checkout With Google Pay</string>
<string name="order_with_googlepay_button_content_description">Order With Google Pay</string>
<string name="view_in_googlepay_button_content_description">View In Google Pay</string>
</resources>

View file

@ -331,4 +331,16 @@ Amount to pay: %s</string>
<string name="feedback_preface_tx_push_failed" translatable="false">Please tell us more about your issue. Every small detail can help.</string>
<string name="feedback_data_collection_message" translatable="false">Following information is optional. You can erase it if you dont want to share it.</string>
<string name="shop_one_wallet" translatable="false">One Wallet</string>
<string name="shop_3_cards" translatable="false">3 cards</string>
<string name="shop_2_cards" translatable="false">2 cards</string>
<string name="shop_shipping" translatable="false">Shipping</string>
<string name="shop_free" translatable="false">Free</string>
<string name="shop_i_have_a_promo_code" translatable="false">I have a promo code...</string>
<string name="shop_total" translatable="false">Total</string>
<string name="shop_other_payment_methods" translatable="false">Other payment methods</string>
<string name="shop_buy_now" translatable="false">Buy now</string>
<string name="shop_title" translatable="false">Order card</string>
<string name="shop_free_delivery" translatable="false">Delivery (Free shipping)</string>
</resources>

View file

@ -147,6 +147,15 @@
<item name="android:windowSoftInputMode">adjustResize</item>
</style>
<style name="ShopChips" parent="@style/Widget.MaterialComponents.Chip.Choice">
<item name="chipBackgroundColor">@color/selector_chip_shop</item>
<item name="android:minWidth">100dp</item>
<item name="android:textAlignment">center</item>
<item name="android:textSize">15sp</item>
<item name="android:textStyle">bold</item>
<item name="android:textColor">@color/selector_chip_shop_text</item>
</style>
</resources>
<!-- android:fontFamily="sans-serif" // roboto regular -->

View file

@ -10,7 +10,7 @@ buildscript {
classpath "com.android.tools.build:gradle:${versions.build_gradle}"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath "com.github.dcendents:android-maven-gradle-plugin:2.1"
classpath 'com.google.gms:google-services:4.3.8'
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'
}
}