diff --git a/app/build.gradle b/app/build.gradle
index 845687d9d8..aa1b8e24b1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f358215715..b38eacf816 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -39,6 +39,9 @@
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:fullBackupContent">
+
{
+ store.dispatch(
+ ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data)
+ )
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt
index 1d84bd399c..74e957f6c3 100644
--- a/app/src/main/java/com/tangem/tap/TapApplication.kt
+++ b/app/src/main/java/com/tangem/tap/TapApplication.kt
@@ -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)) }
}
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
index 30d8e1b18a..5fdd3a9279 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt
@@ -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()
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
index d797ff9010..cbfa2565fb 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
@@ -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),
)
}
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
index d3c6f3656b..719386cad9 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt
@@ -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,
)
}
}
diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
index 61e3d263da..6f68439f61 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
@@ -11,6 +11,7 @@ data class NavigationState(
enum class AppScreen {
Home,
+ Shop,
Disclaimer,
OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther,
Wallet, WalletDetails,
diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt
new file mode 100644
index 0000000000..d428a9d197
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt
@@ -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()
+ private val variants = mutableMapOf()
+
+ private lateinit var googlePayService: GooglePayService
+
+ suspend fun getProducts(): Result> {
+ 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 {
+ 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) -> Unit
+// ) {
+// googlePayService.responseCallback = { result ->
+// result.onFailure { }
+// result.onSuccess {
+// completeTokenizedPayment(it, productType)
+// }
+// }
+// }
+
+ suspend fun handleGooglePayResult(
+ resultCode: Int,
+ data: Intent?,
+ productType: ProductType
+ ): Result {
+ 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 {
+ 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> {
+ 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 {
+ 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
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt b/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt
new file mode 100644
index 0000000000..89116f8096
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt
new file mode 100644
index 0000000000..022af2b4ea
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt
@@ -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
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt
new file mode 100644
index 0000000000..a9e920745f
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt
@@ -0,0 +1,6 @@
+package com.tangem.tap.common.shop.data
+
+data class TotalSum(
+ val finalValue: String? = null,
+ val beforeDiscount: String? = null,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt
new file mode 100644
index 0000000000..13c3913ebe
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt
@@ -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) -> Unit)? = null
+
+ suspend fun checkIfGooglePayAvailable(): Result {
+
+ 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 {
+ 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
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt
new file mode 100644
index 0000000000..610ab10c9d
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt
@@ -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
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt
new file mode 100644
index 0000000000..da24d1337d
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt
@@ -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 {
+ 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> {
+ 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 {
+
+ val query = query { rootQuery: QueryRootQuery ->
+ rootQuery
+ .node(checkoutID) { query ->
+ query.onCheckout { checkoutQuery ->
+ with(checkoutQuery) {
+ checkoutFieldsFragment()
+ }
+ }
+ }
+ }
+ val retryHandler = RetryHandler.build(
+ 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,
+ checkoutID: ID? = null
+ ): Result {
+
+
+ val storefrontLineItems: MutableList = 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+
+ 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 {
+ 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
+ ): GraphCallResult =
+ withContext(Dispatchers.IO) {
+ suspendCoroutine { continuation ->
+ client.queryGraph(query).enqueue(retryHandler = retryHandler) { result ->
+ continuation.resume(result)
+ }
+ }
+ }
+
+ private suspend fun queryAsync(
+ query: QueryRootQuery,
+ ): GraphCallResult =
+ withContext(Dispatchers.IO) {
+ suspendCoroutine { continuation ->
+ client.queryGraph(query).enqueue { result ->
+ continuation.resume(result)
+ }
+ }
+ }
+
+ private suspend fun mutationQueryAsync(query: MutationQuery): GraphCallResult =
+ 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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyShop.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyShop.kt
new file mode 100644
index 0000000000..5fde9c60ff
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyShop.kt
@@ -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,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt
new file mode 100644
index 0000000000..849c1f33c3
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt
new file mode 100644
index 0000000000..5dc508df40
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt
@@ -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
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt
new file mode 100644
index 0000000000..572a7b68c7
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt
@@ -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()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt
index 412537befb..4324891cae 100644
--- a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt
@@ -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,
)
}
diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt
index edbbf6345c..c82d448e7d 100644
--- a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt
+++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt
@@ -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?,
val infuraProjectId: String?,
val appsFlyerDevKey: String,
+ val shopifyShop: ShopifyShop?
)
class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {
diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt
index fb59fee5b6..95eada77e7 100644
--- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt
@@ -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 = { 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)
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt
index b9b31b5495..ef82fcd394 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt
@@ -34,7 +34,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
StoreSubscriber, 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,8 @@ 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 = BackupCardsWidget(leapfrog, deviceScaleFactor) { 200f * deviceScaleFactor }
+ cardsWidget = WalletCardsWidget(leapfrog, deviceScaleFactor) { 200f * deviceScaleFactor }
startPostponedEnterTransition()
view_pager_backup_info.adapter = BackupInfoAdapter()
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt
similarity index 99%
rename from app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt
rename to app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt
index 99519fb349..eb5ae173af 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt
@@ -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,
diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt
new file mode 100644
index 0000000000..9f7e7937b4
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt
@@ -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) : ShopAction()
+ }
+
+ data class ApplyPromoCode(val promoCode: String) : ShopAction() {
+ data class Success(val promoCode: String?, val products: List) : 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()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt
new file mode 100644
index 0000000000..605d5fe9d2
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt
@@ -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 = { 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))
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt
new file mode 100644
index 0000000000..8d4b4ff2a7
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt
new file mode 100644
index 0000000000..a4ead917a3
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt
@@ -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 = 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
+ }
+}
diff --git a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt
new file mode 100644
index 0000000000..27a20c0842
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt
@@ -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 {
+
+
+ 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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt
index a449bb17ea..722b2c7d03 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt
@@ -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.toPendingTransactionsForToken(token: Token, walletAddr
return this.mapNotNull { it.toPendingTransactionForToken(token, walletAddress) }
}
-fun Wallet.getPendingTransactions(): List {
- return recentTransactions.toPendingTransactions(address)
+fun Wallet.getPendingTransactions(type: PendingTransactionType? = null): List {
+ val txs = recentTransactions.toPendingTransactions(address)
+ return when(type) {
+ null -> txs
+ else -> txs.filter { it.type == type }
+ }
}
fun Wallet.hasPendingTransactions(): Boolean {
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt
index df4c9120f7..25da5ead8b 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt
@@ -13,6 +13,8 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
+import com.tangem.tap.features.wallet.models.PendingTransactionType
+import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
@@ -22,6 +24,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import java.math.BigDecimal
class MultiWalletMiddleware {
fun handle(
@@ -206,13 +209,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 -> {}
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt
index 09c6094d38..31ab7c4368 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt
@@ -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) {
diff --git a/app/src/main/res/color/selector_chip_shop.xml b/app/src/main/res/color/selector_chip_shop.xml
new file mode 100644
index 0000000000..052f962892
--- /dev/null
+++ b/app/src/main/res/color/selector_chip_shop.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/selector_chip_shop_text.xml b/app/src/main/res/color/selector_chip_shop_text.xml
new file mode 100644
index 0000000000..5d5afc33ff
--- /dev/null
+++ b/app/src/main/res/color/selector_chip_shop_text.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-de/buy_with_googlepay_button_content.xml b/app/src/main/res/drawable-de/buy_with_googlepay_button_content.xml
new file mode 100755
index 0000000000..e1e2577110
--- /dev/null
+++ b/app/src/main/res/drawable-de/buy_with_googlepay_button_content.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable-fr/buy_with_googlepay_button_content.xml b/app/src/main/res/drawable-fr/buy_with_googlepay_button_content.xml
new file mode 100755
index 0000000000..4be47da569
--- /dev/null
+++ b/app/src/main/res/drawable-fr/buy_with_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable-hdpi/card_placeholder_new.png b/app/src/main/res/drawable-hdpi/card_placeholder_new.png
deleted file mode 100644
index 4c9cff9062..0000000000
Binary files a/app/src/main/res/drawable-hdpi/card_placeholder_new.png and /dev/null differ
diff --git a/app/src/main/res/drawable-hdpi/card_placeholder_wallet.9.png b/app/src/main/res/drawable-hdpi/card_placeholder_wallet.9.png
new file mode 100644
index 0000000000..97ea9bc260
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/card_placeholder_wallet.9.png differ
diff --git a/app/src/main/res/drawable-it/buy_with_googlepay_button_content.xml b/app/src/main/res/drawable-it/buy_with_googlepay_button_content.xml
new file mode 100755
index 0000000000..477793d6e5
--- /dev/null
+++ b/app/src/main/res/drawable-it/buy_with_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable-ldpi/card_placeholder_new.png b/app/src/main/res/drawable-ldpi/card_placeholder_new.png
deleted file mode 100644
index e9986ae27f..0000000000
Binary files a/app/src/main/res/drawable-ldpi/card_placeholder_new.png and /dev/null differ
diff --git a/app/src/main/res/drawable-mdpi/card_placeholder_new.png b/app/src/main/res/drawable-mdpi/card_placeholder_new.png
deleted file mode 100644
index b15e1cf3bb..0000000000
Binary files a/app/src/main/res/drawable-mdpi/card_placeholder_new.png and /dev/null differ
diff --git a/app/src/main/res/drawable-mdpi/card_placeholder_wallet.9.png b/app/src/main/res/drawable-mdpi/card_placeholder_wallet.9.png
new file mode 100644
index 0000000000..8a45ec562c
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/card_placeholder_wallet.9.png differ
diff --git a/app/src/main/res/drawable-ru/buy_with_googlepay_button_content.xml b/app/src/main/res/drawable-ru/buy_with_googlepay_button_content.xml
new file mode 100755
index 0000000000..a624bdabf1
--- /dev/null
+++ b/app/src/main/res/drawable-ru/buy_with_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable-xhdpi/card_placeholder_new.png b/app/src/main/res/drawable-xhdpi/card_placeholder_new.png
deleted file mode 100644
index a9f3c750e0..0000000000
Binary files a/app/src/main/res/drawable-xhdpi/card_placeholder_new.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xhdpi/card_placeholder_wallet.9.png b/app/src/main/res/drawable-xhdpi/card_placeholder_wallet.9.png
new file mode 100644
index 0000000000..ee8fdfd5f2
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/card_placeholder_wallet.9.png differ
diff --git a/app/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png b/app/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png
new file mode 100755
index 0000000000..40f767d533
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png differ
diff --git a/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png b/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png
new file mode 100755
index 0000000000..969cc52a94
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/card_placeholder_new.png b/app/src/main/res/drawable-xxhdpi/card_placeholder_new.png
deleted file mode 100644
index 0caf7a0d35..0000000000
Binary files a/app/src/main/res/drawable-xxhdpi/card_placeholder_new.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xxhdpi/card_placeholder_wallet.9.png b/app/src/main/res/drawable-xxhdpi/card_placeholder_wallet.9.png
new file mode 100644
index 0000000000..7483c7232b
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_placeholder_wallet.9.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png b/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png
new file mode 100755
index 0000000000..91035e2a41
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image.9.png b/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image.9.png
new file mode 100755
index 0000000000..dda66dfd0d
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image.9.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/card_placeholder_new.png b/app/src/main/res/drawable-xxxhdpi/card_placeholder_new.png
deleted file mode 100644
index 56a6e580f0..0000000000
Binary files a/app/src/main/res/drawable-xxxhdpi/card_placeholder_new.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xxxhdpi/card_placeholder_wallet.9.png b/app/src/main/res/drawable-xxxhdpi/card_placeholder_wallet.9.png
new file mode 100644
index 0000000000..3fb1bc1422
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/card_placeholder_wallet.9.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png b/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png
new file mode 100755
index 0000000000..ab905289bc
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png b/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png
new file mode 100755
index 0000000000..e67fdaf516
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png differ
diff --git a/app/src/main/res/drawable/buy_with_googlepay_button_content.xml b/app/src/main/res/drawable/buy_with_googlepay_button_content.xml
new file mode 100755
index 0000000000..a893cd0cac
--- /dev/null
+++ b/app/src/main/res/drawable/buy_with_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/googlepay_button_background.xml b/app/src/main/res/drawable/googlepay_button_background.xml
new file mode 100755
index 0000000000..b81005b06f
--- /dev/null
+++ b/app/src/main/res/drawable/googlepay_button_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/googlepay_button_content.xml b/app/src/main/res/drawable/googlepay_button_content.xml
new file mode 100755
index 0000000000..b2cdb1d52b
--- /dev/null
+++ b/app/src/main/res/drawable/googlepay_button_content.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml b/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml
new file mode 100755
index 0000000000..9543c4bcb6
--- /dev/null
+++ b/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/googlepay_button_overlay.xml b/app/src/main/res/drawable/googlepay_button_overlay.xml
new file mode 100755
index 0000000000..70c7a92271
--- /dev/null
+++ b/app/src/main/res/drawable/googlepay_button_overlay.xml
@@ -0,0 +1,12 @@
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_clear_24.xml b/app/src/main/res/drawable/ic_clear_24.xml
new file mode 100644
index 0000000000..cd2a842a1f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_clear_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_promo_code.xml b/app/src/main/res/drawable/ic_promo_code.xml
new file mode 100644
index 0000000000..149d59df21
--- /dev/null
+++ b/app/src/main/res/drawable/ic_promo_code.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_shipping.xml b/app/src/main/res/drawable/ic_shipping.xml
new file mode 100644
index 0000000000..b36ffe1bed
--- /dev/null
+++ b/app/src/main/res/drawable/ic_shipping.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/shape_line.xml b/app/src/main/res/drawable/shape_line.xml
new file mode 100644
index 0000000000..cf7b6cfb0f
--- /dev/null
+++ b/app/src/main/res/drawable/shape_line.xml
@@ -0,0 +1,8 @@
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shape_rectangle_rounded_4.xml b/app/src/main/res/drawable/shape_rectangle_rounded_4.xml
new file mode 100644
index 0000000000..b7d03c2907
--- /dev/null
+++ b/app/src/main/res/drawable/shape_rectangle_rounded_4.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/buy_with_googlepay_button.xml b/app/src/main/res/layout/buy_with_googlepay_button.xml
new file mode 100755
index 0000000000..2553181fd8
--- /dev/null
+++ b/app/src/main/res/layout/buy_with_googlepay_button.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_onboarding_wallet.xml b/app/src/main/res/layout/fragment_onboarding_wallet.xml
index db8df977bb..2c1c940cb5 100644
--- a/app/src/main/res/layout/fragment_onboarding_wallet.xml
+++ b/app/src/main/res/layout/fragment_onboarding_wallet.xml
@@ -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" />
diff --git a/app/src/main/res/layout/fragment_shop.xml b/app/src/main/res/layout/fragment_shop.xml
new file mode 100644
index 0000000000..ef4297ba3d
--- /dev/null
+++ b/app/src/main/res/layout/fragment_shop.xml
@@ -0,0 +1,375 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/test_shopify_fragment.xml b/app/src/main/res/layout/test_shopify_fragment.xml
new file mode 100644
index 0000000000..eff6a14884
--- /dev/null
+++ b/app/src/main/res/layout/test_shopify_fragment.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 93cbbcca62..3318e7609d 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -29,6 +29,11 @@
#F4F5F6
#DE000000
+ #060606
+ #686868
+ #F2F2F2
+ #090E13
+
#14181D
diff --git a/app/src/main/res/values/googlepay_strings.xml b/app/src/main/res/values/googlepay_strings.xml
new file mode 100755
index 0000000000..81d01c2b74
--- /dev/null
+++ b/app/src/main/res/values/googlepay_strings.xml
@@ -0,0 +1,12 @@
+
+
+ Google Pay
+ Buy with Google Pay
+ Donate With Google Pay
+ Pay With Google Pay
+ Subscribe With Google Pay
+ Book With Google Pay
+ Checkout With Google Pay
+ Order With Google Pay
+ View In Google Pay
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml
index f746827108..d52e9f1f7b 100644
--- a/app/src/main/res/values/strings_untranslated.xml
+++ b/app/src/main/res/values/strings_untranslated.xml
@@ -331,4 +331,16 @@ Amount to pay: %s
Please tell us more about your issue. Every small detail can help.
Following information is optional. You can erase it if you don’t want to share it.
+ One Wallet
+ 3 cards
+ 2 cards
+ Shipping
+ Free
+ I have a promo code...
+ Total
+ Other payment methods
+ Buy now
+ Order card
+ Delivery (Free shipping)
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index ba05fd093a..10c1c9cb80 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -147,6 +147,15 @@
- adjustResize
+
+
diff --git a/build.gradle b/build.gradle
index 0ac373259b..23b9e4d0bc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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'
}
}