diff --git a/app/build.gradle b/app/build.gradle
index 222ad32676..3a4885cc13 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/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..a2244b7442
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyShop.kt
@@ -0,0 +1,7 @@
+package com.tangem.tap.common.shop.shopify
+
+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/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'
}
}