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\"'
|
||||
|
|
@ -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)) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 -> {}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
375
app/src/main/res/layout/fragment_shop.xml
Normal 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>
|
||||
87
app/src/main/res/layout/test_shopify_fragment.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
12
app/src/main/res/values/googlepay_strings.xml
Executable 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>
|
||||
|
|
@ -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 don’t 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>
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||