Updated on 2026-08-14
This commit is contained in:
parent
c98e6195ec
commit
304bc23a3c
15 changed files with 932 additions and 4 deletions
|
|
@ -38,7 +38,7 @@ android {
|
||||||
}
|
}
|
||||||
debug_beta {
|
debug_beta {
|
||||||
initWith release
|
initWith release
|
||||||
debuggable false
|
debuggable true
|
||||||
versionNameSuffix "-beta"
|
versionNameSuffix "-beta"
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
|
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
|
||||||
|
|
@ -129,6 +129,15 @@ dependencies {
|
||||||
// animation
|
// animation
|
||||||
implementation "com.airbnb.android:lottie:3.4.0"
|
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 'junit:junit:4.13.2'
|
||||||
testImplementation "com.google.truth:truth:1.1.3"
|
testImplementation "com.google.truth:truth:1.1.3"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@
|
||||||
tools:ignore="GoogleAppIndexingWarning"
|
tools:ignore="GoogleAppIndexingWarning"
|
||||||
tools:replace="android:fullBackupContent">
|
tools:replace="android:fullBackupContent">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.wallet.api.enabled"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.tangem.tap.MainActivity"
|
android:name="com.tangem.tap.MainActivity"
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,12 @@ import com.tangem.tap.common.redux.global.AndroidResources
|
||||||
import com.tangem.tap.common.redux.global.GlobalAction
|
import com.tangem.tap.common.redux.global.GlobalAction
|
||||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||||
|
import com.tangem.tap.common.shop.GooglePayService
|
||||||
|
import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
|
||||||
|
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
|
||||||
import com.tangem.tap.domain.TangemSdkManager
|
import com.tangem.tap.domain.TangemSdkManager
|
||||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||||
|
import com.tangem.tap.features.shop.redux.ShopAction
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
|
@ -73,8 +77,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
||||||
|
|
||||||
store.dispatch(GlobalAction.SetResources(getAndroidResources()))
|
store.dispatch(GlobalAction.SetResources(getAndroidResources()))
|
||||||
store.dispatch(WalletConnectAction.RestoreSessions)
|
store.dispatch(WalletConnectAction.RestoreSessions)
|
||||||
|
store.dispatch(
|
||||||
|
ShopAction.CheckIfGooglePayAvailable(
|
||||||
|
GooglePayService(createPaymentsClient(this), this)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getAndroidResources(): AndroidResources {
|
private fun getAndroidResources(): AndroidResources {
|
||||||
return AndroidResources(
|
return AndroidResources(
|
||||||
AndroidResources.RString(
|
AndroidResources.RString(
|
||||||
|
|
@ -97,7 +107,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
||||||
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
|
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
|
||||||
val isScannedBefore = store.state.globalState.scanResponse != null
|
val isScannedBefore = store.state.globalState.scanResponse != null
|
||||||
val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted
|
val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted
|
||||||
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore)) {
|
val shopOpened = store.state.shopState.total != null
|
||||||
|
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore && !shopOpened)) {
|
||||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
|
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
|
||||||
}
|
}
|
||||||
intentHandler.handleIntent(intent)
|
intentHandler.handleIntent(intent)
|
||||||
|
|
@ -140,4 +151,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
||||||
snackbar?.dismiss()
|
snackbar?.dismiss()
|
||||||
snackbar = null
|
snackbar = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
when (requestCode) {
|
||||||
|
LOAD_PAYMENT_DATA_REQUEST_CODE -> {
|
||||||
|
store.dispatch(
|
||||||
|
ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import com.tangem.tap.common.images.PicassoHelper
|
||||||
import com.tangem.tap.common.redux.AppState
|
import com.tangem.tap.common.redux.AppState
|
||||||
import com.tangem.tap.common.redux.appReducer
|
import com.tangem.tap.common.redux.appReducer
|
||||||
import com.tangem.tap.common.redux.global.GlobalAction
|
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.ConfigManager
|
||||||
import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
|
import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
|
||||||
import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader
|
import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader
|
||||||
|
|
@ -37,6 +38,7 @@ val store = Store(
|
||||||
lateinit var preferencesStorage: PreferencesStorage
|
lateinit var preferencesStorage: PreferencesStorage
|
||||||
lateinit var currenciesRepository: CurrenciesRepository
|
lateinit var currenciesRepository: CurrenciesRepository
|
||||||
lateinit var walletConnectRepository: WalletConnectRepository
|
lateinit var walletConnectRepository: WalletConnectRepository
|
||||||
|
lateinit var shopService: TangemShopService
|
||||||
|
|
||||||
class TapApplication : Application() {
|
class TapApplication : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
|
@ -72,7 +74,10 @@ class TapApplication : Application() {
|
||||||
val localLoader = FeaturesLocalLoader(this, moshi)
|
val localLoader = FeaturesLocalLoader(this, moshi)
|
||||||
val remoteLoader = FeaturesRemoteLoader(moshi)
|
val remoteLoader = FeaturesRemoteLoader(moshi)
|
||||||
val configManager = ConfigManager(localLoader, remoteLoader)
|
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))
|
val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi))
|
||||||
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
|
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.tangem.tap.common.shop.data
|
||||||
|
|
||||||
|
import com.tangem.tap.common.shop.TangemShopService
|
||||||
|
|
||||||
|
enum class ProductType(val sku: String) {
|
||||||
|
WALLET_2_CARDS(TangemShopService.TANGEM_WALLET_2_CARDS_SKU),
|
||||||
|
WALLET_3_CARDS(TangemShopService.TANGEM_WALLET_3_CARDS_SKU)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.tangem.tap.common.shop.data
|
||||||
|
|
||||||
|
data class TangemProduct(
|
||||||
|
val type: ProductType,
|
||||||
|
val totalSum: TotalSum? = null,
|
||||||
|
val appliedDiscount: String? = null
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.tangem.tap.common.shop.data
|
||||||
|
|
||||||
|
data class TotalSum(
|
||||||
|
val finalValue: String? = null,
|
||||||
|
val beforeDiscount: String? = null,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
package com.tangem.tap.common.shop
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Activity.RESULT_CANCELED
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.android.gms.common.api.ApiException
|
||||||
|
import com.google.android.gms.wallet.*
|
||||||
|
import com.tangem.common.core.TangemSdkError
|
||||||
|
import com.tangem.tap.common.shop.googlepay.GooglePayUtil
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
class GooglePayService(private val paymentsClient: PaymentsClient, private val activity: Activity) {
|
||||||
|
|
||||||
|
// var responseCallback: ((Result<PaymentData>) -> Unit)? = null
|
||||||
|
|
||||||
|
suspend fun checkIfGooglePayAvailable(): Result<Boolean> {
|
||||||
|
|
||||||
|
val isReadyToPayJson = GooglePayUtil.isReadyToPayRequest() ?: return Result.success(false)
|
||||||
|
val request = IsReadyToPayRequest.fromJson(isReadyToPayJson.toString())
|
||||||
|
|
||||||
|
val task = paymentsClient.isReadyToPay(request)
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
suspendCoroutine { continuation ->
|
||||||
|
task.addOnCompleteListener { completedTask ->
|
||||||
|
try {
|
||||||
|
val result = completedTask.getResult(ApiException::class.java)
|
||||||
|
continuation.resume(Result.success(true))
|
||||||
|
} catch (exception: ApiException) {
|
||||||
|
// Process error
|
||||||
|
Timber.w("isReadyToPay failed: $exception")
|
||||||
|
continuation.resume(Result.failure(exception))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun payWithGooglePay(totalPriceCents: String, currencyCode: String, merchantID: String) {
|
||||||
|
val paymentDataRequestJson = GooglePayUtil.getPaymentDataRequest(
|
||||||
|
totalPriceCents,
|
||||||
|
currencyCode = currencyCode,
|
||||||
|
countryCode = "RU",
|
||||||
|
merchantID = merchantID
|
||||||
|
)
|
||||||
|
if (paymentDataRequestJson == null) {
|
||||||
|
Timber.e("RequestPayment: can't fetch payment data request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val request = PaymentDataRequest.fromJson(paymentDataRequestJson.toString())
|
||||||
|
|
||||||
|
AutoResolveHelper.resolveTask(
|
||||||
|
paymentsClient.loadPaymentData(request), activity, LOAD_PAYMENT_DATA_REQUEST_CODE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleResponseFromGooglePay(resultCode: Int, data: Intent?): Result<PaymentData> {
|
||||||
|
val result = when (resultCode) {
|
||||||
|
RESULT_OK -> {
|
||||||
|
val paymentData = data?.let { intent -> PaymentData.getFromIntent(intent) }
|
||||||
|
if (paymentData == null) {
|
||||||
|
Result.failure(Exception("No payment data"))
|
||||||
|
} else {
|
||||||
|
Result.success(paymentData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RESULT_CANCELED -> {
|
||||||
|
Result.failure(TangemSdkError.UserCancelled())
|
||||||
|
}
|
||||||
|
AutoResolveHelper.RESULT_ERROR -> {
|
||||||
|
val statusCode = AutoResolveHelper.getStatusFromIntent(data)?.statusCode
|
||||||
|
if (statusCode == null) {
|
||||||
|
Result.failure(Exception("Unknown Status"))
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("$statusCode"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else -> Result.failure(Exception("Unknown Status"))
|
||||||
|
}
|
||||||
|
// responseCallback?.invoke(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parsePaymentData(paymentData: PaymentData): GooglePayResponse? {
|
||||||
|
val paymentInformation = paymentData.toJson()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Token will be null if PaymentDataRequest was not constructed using fromJson(String).
|
||||||
|
val paymentMethodData =
|
||||||
|
JSONObject(paymentInformation).getJSONObject("paymentMethodData")
|
||||||
|
val addressJson = paymentMethodData.getJSONObject("info")
|
||||||
|
.getJSONObject("billingAddress")
|
||||||
|
|
||||||
|
val address = Address(
|
||||||
|
name = addressJson.getString("name"),
|
||||||
|
postalCode = addressJson.getString("postalCode"),
|
||||||
|
countryCode = addressJson.getString("countryCode"),
|
||||||
|
phoneNumber = addressJson.getString("phoneNumber"),
|
||||||
|
address1 = addressJson.getString("address1"),
|
||||||
|
address2 = addressJson.getString("address2"),
|
||||||
|
address3 = addressJson.getString("address3"),
|
||||||
|
locality = addressJson.getString("locality"),
|
||||||
|
administrativeArea = addressJson.getString("administrativeArea"),
|
||||||
|
sortingCode = addressJson.getString("sortingCode"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val token = paymentMethodData
|
||||||
|
.getJSONObject("tokenizationData")
|
||||||
|
.getString("token")
|
||||||
|
|
||||||
|
return GooglePayResponse(address, token)
|
||||||
|
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Log.e("handlePaymentSuccess", "Error: " + e.toString())
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LOAD_PAYMENT_DATA_REQUEST_CODE = 315
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class GooglePayResponse(
|
||||||
|
val billingAddress: Address,
|
||||||
|
val token: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Address(
|
||||||
|
val name: String,
|
||||||
|
val postalCode: String,
|
||||||
|
val countryCode: String,
|
||||||
|
val phoneNumber: String,
|
||||||
|
val address1: String,
|
||||||
|
val address2: String,
|
||||||
|
val address3: String,
|
||||||
|
val locality: String,
|
||||||
|
val administrativeArea: String,
|
||||||
|
val sortingCode: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
package com.tangem.tap.common.shop.googlepay
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import com.google.android.gms.wallet.PaymentsClient
|
||||||
|
import com.google.android.gms.wallet.Wallet
|
||||||
|
import com.google.android.gms.wallet.WalletConstants
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
object GooglePayUtil {
|
||||||
|
private val baseRequest = JSONObject().apply {
|
||||||
|
put("apiVersion", 2)
|
||||||
|
put("apiVersionMinor", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun gatewayTokenizationSpecification(merchantID: String): JSONObject {
|
||||||
|
return JSONObject().apply {
|
||||||
|
put("type", "PAYMENT_GATEWAY")
|
||||||
|
put(
|
||||||
|
"parameters", JSONObject(
|
||||||
|
mapOf(
|
||||||
|
"gateway" to "shopify",
|
||||||
|
"gatewayMerchantId" to merchantID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val allowedCardNetworks = JSONArray(
|
||||||
|
listOf(
|
||||||
|
"AMEX",
|
||||||
|
"DISCOVER",
|
||||||
|
"INTERAC",
|
||||||
|
"JCB",
|
||||||
|
"MASTERCARD",
|
||||||
|
"VISA"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val allowedCardAuthMethods = JSONArray(
|
||||||
|
listOf(
|
||||||
|
"PAN_ONLY",
|
||||||
|
"CRYPTOGRAM_3DS"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun baseCardPaymentMethod(): JSONObject {
|
||||||
|
return JSONObject().apply {
|
||||||
|
|
||||||
|
val parameters = JSONObject().apply {
|
||||||
|
put("allowedAuthMethods", allowedCardAuthMethods)
|
||||||
|
put("allowedCardNetworks", allowedCardNetworks)
|
||||||
|
put("billingAddressRequired", true)
|
||||||
|
put("billingAddressParameters", JSONObject().apply {
|
||||||
|
put("format", "FULL")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
put("type", "CARD")
|
||||||
|
put("parameters", parameters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cardPaymentMethod(merchantID: String): JSONObject {
|
||||||
|
val cardPaymentMethod = baseCardPaymentMethod()
|
||||||
|
cardPaymentMethod.put("tokenizationSpecification", gatewayTokenizationSpecification(merchantID))
|
||||||
|
|
||||||
|
return cardPaymentMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPaymentsClient(activity: Activity): PaymentsClient {
|
||||||
|
val walletOptions = Wallet.WalletOptions.Builder()
|
||||||
|
.setEnvironment(PAYMENTS_ENVIRONMENT)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return Wallet.getPaymentsClient(activity, walletOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isReadyToPayRequest(): JSONObject? {
|
||||||
|
return try {
|
||||||
|
baseRequest.apply {
|
||||||
|
put("allowedPaymentMethods", JSONArray().put(baseCardPaymentMethod()))
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTransactionInfo(
|
||||||
|
price: String,
|
||||||
|
countryCode: String,
|
||||||
|
currencyCode: String
|
||||||
|
): JSONObject {
|
||||||
|
return JSONObject().apply {
|
||||||
|
put("totalPrice", price)
|
||||||
|
put("totalPriceStatus", "FINAL")
|
||||||
|
put("countryCode", countryCode)
|
||||||
|
put("currencyCode", currencyCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val merchantInfo: JSONObject =
|
||||||
|
JSONObject().put("merchantName", "Example Merchant")
|
||||||
|
|
||||||
|
|
||||||
|
fun getPaymentDataRequest(
|
||||||
|
price: String,
|
||||||
|
countryCode: String,
|
||||||
|
currencyCode: String,
|
||||||
|
merchantID: String
|
||||||
|
): JSONObject? {
|
||||||
|
try {
|
||||||
|
return baseRequest.apply {
|
||||||
|
put("allowedPaymentMethods", JSONArray().put(cardPaymentMethod(merchantID)))
|
||||||
|
put("transactionInfo", getTransactionInfo(price, countryCode, currencyCode))
|
||||||
|
put("merchantInfo", merchantInfo)
|
||||||
|
|
||||||
|
// An optional shipping address requirement is a top-level property of the
|
||||||
|
// PaymentDataRequest JSON object.
|
||||||
|
val shippingAddressParameters = JSONObject().apply {
|
||||||
|
put("phoneNumberRequired", false)
|
||||||
|
// put("allowedCountryCodes", JSONArray(listOf("US", "GB")))
|
||||||
|
}
|
||||||
|
put("shippingAddressParameters", shippingAddressParameters)
|
||||||
|
put("shippingAddressRequired", true)
|
||||||
|
}
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const val PAYMENTS_ENVIRONMENT = WalletConstants.ENVIRONMENT_TEST
|
||||||
|
|
@ -0,0 +1,384 @@
|
||||||
|
package com.tangem.tap.common.shop
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.shopify.buy3.GraphCallResult
|
||||||
|
import com.shopify.buy3.GraphClient
|
||||||
|
import com.shopify.buy3.RetryHandler
|
||||||
|
import com.shopify.buy3.Storefront.*
|
||||||
|
import com.shopify.graphql.support.ID
|
||||||
|
import com.shopify.graphql.support.Input
|
||||||
|
import com.tangem.tap.common.shop.shopify.ShopifyShop
|
||||||
|
import com.tangem.tap.common.shop.shopify.data.CheckoutItem
|
||||||
|
import com.tangem.tap.common.shop.shopify.data.checkoutFieldsFragment
|
||||||
|
import com.tangem.tap.common.shop.shopify.data.collectionFieldsFragment
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
|
||||||
|
class ShopifyService(private val application: Application, val shop: ShopifyShop) {
|
||||||
|
val client: GraphClient by lazy { initClient() }
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun getShopName(): Result<String> {
|
||||||
|
val query = query { rootQuery: QueryRootQuery ->
|
||||||
|
rootQuery
|
||||||
|
.shop { shopQuery: ShopQuery ->
|
||||||
|
shopQuery
|
||||||
|
.name()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return when (val result = queryAsync(query)) {
|
||||||
|
is GraphCallResult.Success -> {
|
||||||
|
val name = result.response.data!!.shop.name
|
||||||
|
Result.success(name)
|
||||||
|
}
|
||||||
|
is GraphCallResult.Failure -> {
|
||||||
|
Result.failure(result.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getProducts(collectionTitleFilter: String? = null): Result<List<Product>> {
|
||||||
|
val filter = collectionTitleFilter?.let { "title:\"$it\"" }
|
||||||
|
|
||||||
|
val query = query { rootQuery: QueryRootQuery ->
|
||||||
|
rootQuery
|
||||||
|
.collections(
|
||||||
|
{ arg -> arg.first(250).query(filter) },
|
||||||
|
) { collectionConnectionQuery ->
|
||||||
|
collectionConnectionQuery.collectionFieldsFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return when (val result = queryAsync(query)) {
|
||||||
|
is GraphCallResult.Success -> {
|
||||||
|
val products = result.response.data!!.collections.edges
|
||||||
|
.map { it.node.products }
|
||||||
|
.flatMap { it.edges }
|
||||||
|
.map { it.node }
|
||||||
|
Result.success(products)
|
||||||
|
}
|
||||||
|
is GraphCallResult.Failure -> {
|
||||||
|
Result.failure(result.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun checkout(pollUntilOrder: Boolean, checkoutID: ID): Result<Checkout> {
|
||||||
|
|
||||||
|
val query = query { rootQuery: QueryRootQuery ->
|
||||||
|
rootQuery
|
||||||
|
.node(checkoutID) { query ->
|
||||||
|
query.onCheckout { checkoutQuery ->
|
||||||
|
with(checkoutQuery) {
|
||||||
|
checkoutFieldsFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val retryHandler = RetryHandler.build<QueryRoot>(
|
||||||
|
1, TimeUnit.SECONDS
|
||||||
|
) {
|
||||||
|
this.retryWhen { result ->
|
||||||
|
when (result) {
|
||||||
|
is GraphCallResult.Success -> {
|
||||||
|
val checkout = result.response.data?.node as? Checkout
|
||||||
|
checkout == null
|
||||||
|
}
|
||||||
|
is GraphCallResult.Failure -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = if (pollUntilOrder) queryAsync(query, retryHandler) else queryAsync(query)
|
||||||
|
return when (result) {
|
||||||
|
is GraphCallResult.Success -> {
|
||||||
|
val checkout = result.response.data!!.node as? Checkout
|
||||||
|
if (checkout != null) {
|
||||||
|
Result.success(checkout)
|
||||||
|
} else {
|
||||||
|
Result.failure(ShopifyError.Unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
is GraphCallResult.Failure -> {
|
||||||
|
Result.failure(result.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createCheckout(
|
||||||
|
checkoutItems: List<CheckoutItem>,
|
||||||
|
checkoutID: ID? = null
|
||||||
|
): Result<Checkout> {
|
||||||
|
|
||||||
|
|
||||||
|
val storefrontLineItems: MutableList<CheckoutLineItemInput> = checkoutItems
|
||||||
|
.map { CheckoutLineItemInput(it.quantity, it.id) }.toMutableList()
|
||||||
|
|
||||||
|
val query = if (checkoutID != null) {
|
||||||
|
mutation { mutationQuery: MutationQuery ->
|
||||||
|
mutationQuery
|
||||||
|
.checkoutLineItemsReplace(
|
||||||
|
storefrontLineItems, checkoutID
|
||||||
|
) { payloadQuery: CheckoutLineItemsReplacePayloadQuery ->
|
||||||
|
payloadQuery
|
||||||
|
.checkout { checkoutQuery: CheckoutQuery ->
|
||||||
|
checkoutQuery.checkoutFieldsFragment()
|
||||||
|
}
|
||||||
|
.userErrors() { userErrorQuery: CheckoutUserErrorQuery ->
|
||||||
|
userErrorQuery
|
||||||
|
.field()
|
||||||
|
.message()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val input = CheckoutCreateInput()
|
||||||
|
.setLineItemsInput(
|
||||||
|
Input.value(storefrontLineItems)
|
||||||
|
)
|
||||||
|
mutation { mutationQuery: MutationQuery ->
|
||||||
|
mutationQuery
|
||||||
|
.checkoutCreate(
|
||||||
|
input
|
||||||
|
) { payloadQuery: CheckoutCreatePayloadQuery ->
|
||||||
|
payloadQuery
|
||||||
|
.checkout { checkoutQuery: CheckoutQuery ->
|
||||||
|
checkoutQuery.checkoutFieldsFragment()
|
||||||
|
}
|
||||||
|
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
|
||||||
|
userErrorQuery
|
||||||
|
.field()
|
||||||
|
.message()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runCheckoutMutation(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun applyDiscount(discountCode: String, checkoutID: ID): Result<Checkout> {
|
||||||
|
val query = mutation { mutationQuery: MutationQuery ->
|
||||||
|
mutationQuery
|
||||||
|
.checkoutDiscountCodeApplyV2(
|
||||||
|
discountCode, checkoutID
|
||||||
|
) { payloadQuery: CheckoutDiscountCodeApplyV2PayloadQuery ->
|
||||||
|
payloadQuery
|
||||||
|
.checkout { checkoutQuery: CheckoutQuery ->
|
||||||
|
checkoutQuery.checkoutFieldsFragment()
|
||||||
|
}
|
||||||
|
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
|
||||||
|
userErrorQuery
|
||||||
|
.field()
|
||||||
|
.message()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runCheckoutMutation(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun removeDiscount(checkoutID: ID): Result<Checkout> {
|
||||||
|
val query = mutation { mutationQuery: MutationQuery ->
|
||||||
|
mutationQuery
|
||||||
|
.checkoutDiscountCodeRemove(
|
||||||
|
checkoutID
|
||||||
|
) { payloadQuery: CheckoutDiscountCodeRemovePayloadQuery ->
|
||||||
|
payloadQuery
|
||||||
|
.checkout { checkoutQuery: CheckoutQuery ->
|
||||||
|
checkoutQuery.checkoutFieldsFragment()
|
||||||
|
}
|
||||||
|
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
|
||||||
|
userErrorQuery
|
||||||
|
.field()
|
||||||
|
.message()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runCheckoutMutation(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateAddress(
|
||||||
|
address: MailingAddress,
|
||||||
|
checkoutID: ID,
|
||||||
|
waitForShippingRates: Boolean
|
||||||
|
): Result<Checkout> {
|
||||||
|
val input = MailingAddressInput()
|
||||||
|
.setAddress1(address.address1)
|
||||||
|
.setAddress2(address.address2)
|
||||||
|
.setCity(address.city)
|
||||||
|
.setCountry(address.country)
|
||||||
|
.setFirstName(address.firstName)
|
||||||
|
.setLastName(address.lastName)
|
||||||
|
.setPhone(address.phone)
|
||||||
|
.setProvince(address.province)
|
||||||
|
.setZip(address.zip)
|
||||||
|
|
||||||
|
val query = mutation { mutationQuery: MutationQuery ->
|
||||||
|
mutationQuery
|
||||||
|
.checkoutShippingAddressUpdateV2(
|
||||||
|
input, checkoutID
|
||||||
|
) { shippingAddressUpdatePayloadQuery: CheckoutShippingAddressUpdateV2PayloadQuery ->
|
||||||
|
shippingAddressUpdatePayloadQuery
|
||||||
|
.checkout { checkoutQuery: CheckoutQuery ->
|
||||||
|
checkoutQuery.checkoutFieldsFragment()
|
||||||
|
}
|
||||||
|
.checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery ->
|
||||||
|
userErrorQuery
|
||||||
|
.field()
|
||||||
|
.message()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return runCheckoutMutation(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateEmail(email: String?, checkoutID: ID): Result<Checkout> {
|
||||||
|
val query = mutation { mutationQuery: MutationQuery ->
|
||||||
|
mutationQuery
|
||||||
|
.checkoutEmailUpdateV2(
|
||||||
|
checkoutID, email
|
||||||
|
) { emailUpdatePayloadQuery: CheckoutEmailUpdateV2PayloadQuery ->
|
||||||
|
emailUpdatePayloadQuery
|
||||||
|
.checkout { checkoutQuery: CheckoutQuery ->
|
||||||
|
checkoutQuery.checkoutFieldsFragment()
|
||||||
|
}
|
||||||
|
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
|
||||||
|
userErrorQuery
|
||||||
|
.field()
|
||||||
|
.message()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runCheckoutMutation(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateShippingRate(handle: String?, checkoutID: ID): Result<Checkout> {
|
||||||
|
val query = mutation { mutationQuery: MutationQuery ->
|
||||||
|
mutationQuery
|
||||||
|
.checkoutShippingLineUpdate(
|
||||||
|
checkoutID, handle
|
||||||
|
) { shippingLineUpdatePayloadQuery: CheckoutShippingLineUpdatePayloadQuery ->
|
||||||
|
shippingLineUpdatePayloadQuery
|
||||||
|
.checkout { checkoutQuery: CheckoutQuery ->
|
||||||
|
checkoutQuery.checkoutFieldsFragment()
|
||||||
|
}
|
||||||
|
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
|
||||||
|
userErrorQuery
|
||||||
|
.field()
|
||||||
|
.message()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runCheckoutMutation(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun completeWithTokenizedPayment(
|
||||||
|
payment: TokenizedPaymentInputV3,
|
||||||
|
checkoutID: ID
|
||||||
|
): Result<Checkout> {
|
||||||
|
|
||||||
|
val query = mutation { mutationQuery: MutationQuery ->
|
||||||
|
mutationQuery
|
||||||
|
.checkoutCompleteWithTokenizedPaymentV3(
|
||||||
|
checkoutID, payment
|
||||||
|
) { payloadQuery: CheckoutCompleteWithTokenizedPaymentV3PayloadQuery ->
|
||||||
|
payloadQuery
|
||||||
|
.payment { paymentQuery: PaymentQuery ->
|
||||||
|
paymentQuery
|
||||||
|
.ready()
|
||||||
|
.errorMessage()
|
||||||
|
}
|
||||||
|
.checkout { checkoutQuery: CheckoutQuery ->
|
||||||
|
checkoutQuery
|
||||||
|
.ready()
|
||||||
|
}
|
||||||
|
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
|
||||||
|
userErrorQuery
|
||||||
|
.field()
|
||||||
|
.message()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runCheckoutMutation(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startGooglePaySession() {
|
||||||
|
// PaySession()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun runCheckoutMutation(mutation: MutationQuery): Result<Checkout> {
|
||||||
|
return when (val result = mutationQueryAsync(mutation)) {
|
||||||
|
is GraphCallResult.Success -> {
|
||||||
|
val checkout = result.response.data!!.checkoutCreate?.checkout
|
||||||
|
?: result.response.data!!.checkoutDiscountCodeApplyV2?.checkout
|
||||||
|
?: result.response.data!!.checkoutDiscountCodeRemove?.checkout
|
||||||
|
?: result.response.data!!.checkoutShippingAddressUpdateV2?.checkout
|
||||||
|
?: result.response.data!!.checkoutEmailUpdateV2?.checkout
|
||||||
|
?: result.response.data!!.checkoutShippingLineUpdate?.checkout
|
||||||
|
?: result.response.data!!.checkoutCompleteWithTokenizedPaymentV3.checkout
|
||||||
|
|
||||||
|
Result.success(checkout)
|
||||||
|
}
|
||||||
|
is GraphCallResult.Failure -> Result.failure(result.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun queryAsync(
|
||||||
|
query: QueryRootQuery,
|
||||||
|
retryHandler: RetryHandler<QueryRoot>
|
||||||
|
): GraphCallResult<QueryRoot> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
suspendCoroutine { continuation ->
|
||||||
|
client.queryGraph(query).enqueue(retryHandler = retryHandler) { result ->
|
||||||
|
continuation.resume(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun queryAsync(
|
||||||
|
query: QueryRootQuery,
|
||||||
|
): GraphCallResult<QueryRoot> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
suspendCoroutine { continuation ->
|
||||||
|
client.queryGraph(query).enqueue { result ->
|
||||||
|
continuation.resume(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mutationQueryAsync(query: MutationQuery): GraphCallResult<Mutation> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
suspendCoroutine { continuation ->
|
||||||
|
client.mutateGraph(query).enqueue { result ->
|
||||||
|
continuation.resume(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initClient(): GraphClient {
|
||||||
|
return GraphClient.build(
|
||||||
|
application,
|
||||||
|
shop.domain,
|
||||||
|
shop.storefrontApiKey
|
||||||
|
) {
|
||||||
|
// httpCache(application.filesDir) {
|
||||||
|
// cacheMaxSizeBytes = (1024 * 1024 * 10)
|
||||||
|
// defaultCachePolicy =
|
||||||
|
// HttpCachePolicy.Default.CACHE_FIRST.expireAfter(20, TimeUnit.MINUTES)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sealed class ShopifyError : Throwable() {
|
||||||
|
object Unknown : ShopifyError()
|
||||||
|
object GooglePayFailed : ShopifyError()
|
||||||
|
class UserError(val errorMessage: String)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.tangem.tap.common.shop.shopify
|
||||||
|
|
||||||
|
data class ShopifyShop(
|
||||||
|
val domain: String,
|
||||||
|
val storefrontApiKey: String,
|
||||||
|
val merchantID: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
package com.tangem.tap.common.shop.shopify.data
|
||||||
|
|
||||||
|
import com.shopify.buy3.Storefront
|
||||||
|
|
||||||
|
fun Storefront.CheckoutQuery.checkoutFieldsFragment() {
|
||||||
|
// id()
|
||||||
|
ready()
|
||||||
|
webUrl()
|
||||||
|
currencyCode()
|
||||||
|
lineItemsSubtotalPrice { it.amount() }
|
||||||
|
totalPriceV2 {
|
||||||
|
it.currencyCode()
|
||||||
|
it.amount() }
|
||||||
|
lineItems({ arg -> arg.first(250) }) {
|
||||||
|
it.edges {
|
||||||
|
it.node {
|
||||||
|
// it.id()
|
||||||
|
it.title()
|
||||||
|
it.quantity()
|
||||||
|
it.variant() {
|
||||||
|
it.priceV2() { it.amount() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shippingLine {
|
||||||
|
it.handle()
|
||||||
|
it.title()
|
||||||
|
it.priceV2 { it.amount() }
|
||||||
|
}
|
||||||
|
availableShippingRates {
|
||||||
|
it.ready()
|
||||||
|
it.shippingRates {
|
||||||
|
it.handle()
|
||||||
|
it.title()
|
||||||
|
it.priceV2 { it.amount() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shippingAddress {
|
||||||
|
it.address1()
|
||||||
|
it.address2()
|
||||||
|
it.city()
|
||||||
|
// .company()
|
||||||
|
it.country()
|
||||||
|
// .countryCodeV2()
|
||||||
|
it.firstName()
|
||||||
|
// .formatted()
|
||||||
|
// .formattedArea()
|
||||||
|
// .id()
|
||||||
|
it.lastName()
|
||||||
|
// .latitude()
|
||||||
|
// .longitude()
|
||||||
|
// .name()
|
||||||
|
it.phone()
|
||||||
|
it.province()
|
||||||
|
// .provinceCode()
|
||||||
|
it.zip()
|
||||||
|
}
|
||||||
|
discountApplications({ arg -> arg.first(250) }) {
|
||||||
|
it.edges {
|
||||||
|
it.node {
|
||||||
|
it.onDiscountCodeApplication {
|
||||||
|
it.code()
|
||||||
|
// .applicable()
|
||||||
|
// .allocationMethod()
|
||||||
|
// .targetSelection()
|
||||||
|
// .targetType()
|
||||||
|
it.value {
|
||||||
|
it.onMoneyV2 {
|
||||||
|
it.amount()
|
||||||
|
}
|
||||||
|
it.onPricingPercentageValue {
|
||||||
|
it.percentage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
order {
|
||||||
|
it.cancelReason()
|
||||||
|
it.canceledAt()
|
||||||
|
it.currencyCode()
|
||||||
|
// .currentSubtotalPrice()
|
||||||
|
// .currentTotalDuties()
|
||||||
|
// .currentTotalPrice()
|
||||||
|
// .currentTotalTax()
|
||||||
|
it.customerLocale()
|
||||||
|
it.customerUrl()
|
||||||
|
// .discountApplications()
|
||||||
|
it.edited()
|
||||||
|
it.email()
|
||||||
|
it.financialStatus()
|
||||||
|
it.fulfillmentStatus()
|
||||||
|
// .id()
|
||||||
|
// .lineItems()
|
||||||
|
// .metafield()
|
||||||
|
// .metafields()
|
||||||
|
it.name()
|
||||||
|
it.orderNumber()
|
||||||
|
// .originalTotalDuties()
|
||||||
|
// .originalTotalPrice()
|
||||||
|
it.phone()
|
||||||
|
it.processedAt()
|
||||||
|
it.shippingAddress {
|
||||||
|
it.address1()
|
||||||
|
it.address2()
|
||||||
|
it.city()
|
||||||
|
it.company()
|
||||||
|
it.country()
|
||||||
|
it.countryCodeV2()
|
||||||
|
it.firstName()
|
||||||
|
it.formatted()
|
||||||
|
it.formattedArea()
|
||||||
|
// .id()
|
||||||
|
it.lastName()
|
||||||
|
it.latitude()
|
||||||
|
it.longitude()
|
||||||
|
it.name()
|
||||||
|
it.phone()
|
||||||
|
it.province()
|
||||||
|
it.provinceCode()
|
||||||
|
it.zip()
|
||||||
|
}
|
||||||
|
// .shippingDiscountAllocations()
|
||||||
|
it.statusUrl()
|
||||||
|
// .subtotalPriceV2()
|
||||||
|
// .successfulFulfillments()
|
||||||
|
// .totalPriceV2()
|
||||||
|
// .totalRefundedV2()
|
||||||
|
// .totalShippingPriceV2()
|
||||||
|
// .totalTaxV2()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.tangem.tap.common.shop.shopify.data
|
||||||
|
|
||||||
|
import com.shopify.graphql.support.ID
|
||||||
|
|
||||||
|
data class CheckoutItem(
|
||||||
|
val id: ID,
|
||||||
|
val quantity: Int
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.tangem.tap.common.shop.shopify.data
|
||||||
|
|
||||||
|
import com.shopify.buy3.Storefront
|
||||||
|
|
||||||
|
fun Storefront.CollectionConnectionQuery.collectionFieldsFragment() {
|
||||||
|
edges { collectionEdgeQuery ->
|
||||||
|
collectionEdgeQuery
|
||||||
|
.node { collectionQuery ->
|
||||||
|
collectionQuery
|
||||||
|
.title()
|
||||||
|
.products({ arg -> arg.first(250) }
|
||||||
|
) { productConnectionQuery ->
|
||||||
|
productConnectionQuery
|
||||||
|
.edges { productEdgeQuery ->
|
||||||
|
productEdgeQuery
|
||||||
|
.node { productQuery ->
|
||||||
|
productQuery.title()
|
||||||
|
.productType()
|
||||||
|
.description()
|
||||||
|
.variants({ arg -> arg.first(10) }) { variantConnectionQuery ->
|
||||||
|
variantConnectionQuery.edges { variantQuery ->
|
||||||
|
variantQuery.node {
|
||||||
|
it.title()
|
||||||
|
it.sku()
|
||||||
|
it.currentlyNotInStock()
|
||||||
|
it.priceV2 {
|
||||||
|
it.amount()
|
||||||
|
it.currencyCode()
|
||||||
|
}
|
||||||
|
it.compareAtPriceV2 {
|
||||||
|
it.amount()
|
||||||
|
it.currencyCode()
|
||||||
|
}
|
||||||
|
it.compareAtPriceV2 {
|
||||||
|
it.amount()
|
||||||
|
}
|
||||||
|
it.product {
|
||||||
|
it.title()
|
||||||
|
.productType()
|
||||||
|
.description()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ buildscript {
|
||||||
classpath "com.android.tools.build:gradle:${versions.build_gradle}"
|
classpath "com.android.tools.build:gradle:${versions.build_gradle}"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
|
||||||
classpath "com.github.dcendents:android-maven-gradle-plugin:2.1"
|
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'
|
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue