Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-06 11:54:19 +01:00
parent 996356ef61
commit af4a278e88
43 changed files with 1 additions and 2286 deletions

View file

@ -183,10 +183,6 @@ dependencies {
implementation(deps.kotsonGson)
implementation(deps.spongecastle.core)
implementation(deps.lottie)
implementation(deps.shopify.buy) {
exclude(group = "com.shopify.graphql.support")
exclude(module = "joda-time")
}
implementation(deps.compose.accompanist.appCompatTheme)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.webView)

View file

@ -52,9 +52,6 @@ import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.shop.googlepay.GooglePayService
import com.tangem.tap.common.shop.googlepay.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.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
@ -64,7 +61,6 @@ import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHan
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.features.main.model.Toast
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
@ -173,8 +169,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
setContentView(R.layout.activity_main)
initContent()
checkGooglePayAvailability()
checkForNotificationPermission()
observeStateUpdates()
@ -256,14 +250,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
private fun checkGooglePayAvailability() {
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
GooglePayService(createPaymentsClient(this), this),
),
)
}
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode?> {
val tapApplication = application as TapApplication
@ -377,13 +363,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
super.onActivityResult(requestCode, resultCode, data)
onActivityResultCallbacks.forEach { it(requestCode, resultCode, data) }
when (requestCode) {
LOAD_PAYMENT_DATA_REQUEST_CODE -> {
store.dispatch(
ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data),
)
}
}
}
override fun addOnActivityResultCallback(callback: OnActivityResultCallback) {
@ -432,10 +411,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isNotScannedBefore = store.state.globalState.scanResponse == null
val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted
val isShopNotOpened = store.state.shopState.total != null
when {
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive && isShopNotOpened -> {
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> {
navigateToInitialScreen(intentWhichStartedActivity)
}
backStackIsEmpty -> {

View file

@ -59,7 +59,6 @@ import com.tangem.tap.common.log.TimberFormatStrategy
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.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.product.DerivationsFinder
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
@ -84,7 +83,6 @@ lateinit var foregroundActivityObserver: ForegroundActivityObserver
lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesDataSource
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
internal lateinit var derivationsFinder: DerivationsFinder
@HiltAndroidApp
@ -300,7 +298,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
}
private fun initWithConfigDependency(config: Config) {
shopService = TangemShopService(this, config.shopify!!)
initAnalytics(this, config)
initFeedbackManager(this, foregroundActivityObserver, store)
}

View file

@ -1,28 +0,0 @@
package com.tangem.tap.common.analytics.converters
import com.shopify.buy3.Storefront
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.utils.converter.Converter
/**
[REDACTED_AUTHOR]
*/
class ShopOrderToEventConverter : Converter<Pair<Storefront.Checkout, ProductType>, Shop.Purchased> {
override fun convert(value: Pair<Storefront.Checkout, ProductType>): Shop.Purchased {
val checkout = value.first
val productType = value.second
val sku = checkout.lineItems?.edges?.firstOrNull()?.node?.variant?.sku ?: productType.sku
val count = when (productType) {
ProductType.WALLET_2_CARDS -> "2"
ProductType.WALLET_3_CARDS -> "3"
}
val amount = "${checkout.totalPriceV2.amount} ${checkout.totalPriceV2.currencyCode.name}"
val code = (checkout.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)
?.code
return Shop.Purchased(sku, count, amount, code)
}
}

View file

@ -24,7 +24,6 @@ import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragm
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -122,7 +121,6 @@ fun FragmentActivity.getPreviousScreen(): AppScreen? {
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
AppScreen.Home -> HomeFragment()
AppScreen.Shop -> ShopFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()

View file

@ -12,7 +12,6 @@ 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.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.signin.redux.SignInReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
@ -37,7 +36,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
shopState = ShopReducer.reduce(action, state.shopState),
welcomeState = WelcomeReducer.reduce(action, state),
saveWalletState = SaveWalletReducer.reduce(action, state),
signInState = SignInReducer.reduce(action, state),

View file

@ -29,8 +29,6 @@ import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
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.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.tokens.legacy.redux.TokensState
@ -56,7 +54,6 @@ data class AppState(
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val shopState: ShopState = ShopState(),
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
val signInState: SignInState = SignInState(),
@ -92,7 +89,6 @@ data class AppState(
DisclaimerMiddleware().disclaimerMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,
WelcomeMiddleware().middleware,
SaveWalletMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,

View file

@ -1,222 +0,0 @@
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.core.analytics.Analytics
import com.tangem.datasource.config.models.ShopifyShop
import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.common.shop.data.TangemProduct
import com.tangem.tap.common.shop.data.TotalSum
import com.tangem.tap.common.shop.googlepay.GooglePayService
import com.tangem.tap.common.shop.shopify.ShopifyService
import com.tangem.tap.common.shop.shopify.data.CheckoutItem
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.tap.features.shop.domain.models.ProductType.Companion.SKUS_TO_DISPLAY
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
import java.util.Currency
import java.util.UUID
class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
private val shopifyService = ShopifyService(application, shopifyShop)
private val checkouts = mutableMapOf<ProductType, Storefront.Checkout>()
private val variants = mutableMapOf<ProductType, Storefront.ProductVariant>()
private lateinit var googlePayService: GooglePayService
suspend fun getProducts(): Result<List<TangemProduct>> {
val result = shopifyService.getProducts()
return result.mapCatching { product ->
val availableVariants = product
.flatMap { it.variants.edges.map { it.node } }
.filter { SKUS_TO_DISPLAY.contains(it.sku) }
.associateBy { ProductType.fromSku(it.sku) }
.filterNotNull()
variants.putAll(availableVariants)
if (variants.size < SKUS_TO_DISPLAY.size) {
return Result.failure(
Exception(
"Shopify: products are missing, " +
"\nproducts available: ${variants.keys.map { it.sku }}",
),
)
}
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))
}
}
private suspend fun createCheckouts() {
variants.keys.map { coroutineScope { async { createCheckout(it) } } }.awaitAll()
}
private suspend fun createCheckout(productType: ProductType) {
val checkoutItem = CheckoutItem(variants[productType]!!.id, 1)
val result = shopifyService.createCheckout(listOf(checkoutItem))
result.onSuccess { checkout ->
checkouts[productType] = checkout
}
}
suspend fun checkIfGooglePayAvailable(googlePayService: GooglePayService): Result<Boolean> {
this.googlePayService = googlePayService
return googlePayService.checkIfGooglePayAvailable(shopifyService.shop.merchantID != null)
}
fun buyWithGooglePay(productType: ProductType) {
shopifyService.shop.merchantID?.let { merchantId ->
val totalPrice = checkouts[productType]!!.totalPriceV2.amount
googlePayService.payWithGooglePay(
totalPriceCents = totalPrice,
currencyCode = checkouts[productType]!!.currencyCode.name,
merchantID = merchantId,
)
}
}
// fun subscribeToGooglePayResult(
// productType: ProductType,
// resultCallback: (Result<PaymentData>) -> Unit
// ) {
// googlePayService.responseCallback = { result ->
// result.onFailure { }
// result.onSuccess {
// completeTokenizedPayment(it, productType)
// }
// }
// }
suspend fun handleGooglePayResult(resultCode: Int, data: Intent?, productType: ProductType): Result<Unit> {
val result = googlePayService.handleResponseFromGooglePay(resultCode, data)
result.onSuccess {
val finalizePaymentResult = completeTokenizedPayment(it, productType)
finalizePaymentResult.onSuccess {
return Result.success(Unit)
}
return Result.failure(finalizePaymentResult.exceptionOrNull()!!)
}
return Result.failure(result.exceptionOrNull()!!)
}
private suspend fun completeTokenizedPayment(
paymentData: PaymentData,
productType: ProductType,
): Result<Storefront.Checkout> {
val checkout = checkouts[productType]!!
val googlePayResponse =
googlePayService.parsePaymentData(paymentData)
?: return Result.failure(Exception("cannot parse GPay result"))
val amount =
Storefront.MoneyInput(checkout.totalPriceV2.amount, checkout.totalPriceV2.currencyCode)
val idempotencyKey = UUID.randomUUID().toString()
val addressGPay = googlePayResponse.billingAddress
val address = Storefront.MailingAddressInput().apply {
lastName = addressGPay.name
address1 = addressGPay.address1
address2 = addressGPay.address2 + addressGPay.address3
province = addressGPay.administrativeArea
zip = addressGPay.postalCode
phone = addressGPay.phoneNumber
}
val payment = Storefront.TokenizedPaymentInputV3(
amount,
idempotencyKey,
address,
paymentData.toJson(),
Storefront.PaymentTokenType.GOOGLE_PAY,
)
.setTest(true)
return shopifyService.completeWithTokenizedPayment(
payment = payment,
checkoutID = checkout.id,
)
}
suspend fun applyPromoCode(promoCode: String): Result<List<TangemProduct>> {
val products = variants.keys
.map { coroutineScope { async { applyPromoCode(promoCode, it) } } }
.awaitAll()
.map { result -> result.getOrElse { return Result.failure(it) } }
return Result.success(products)
}
private suspend fun applyPromoCode(promoCode: String, productType: ProductType): Result<TangemProduct> {
val checkout = checkouts[productType] ?: return Result.failure(Exception("No checkout"))
val result = if (promoCode.isBlank()) {
shopifyService.removeDiscount(checkout.id)
} else {
shopifyService.applyDiscount(promoCode, checkout.id)
}
result.onSuccess {
checkouts[productType] = it
return Result.success(
TangemProduct(
productType,
TotalSum(
finalValue = it.totalPriceV2.format(),
beforeDiscount = variants[productType]!!.compareAtPriceV2.format(),
),
appliedDiscount = it.getAppliedDiscount(),
),
)
}
return Result.failure(result.exceptionOrNull()!!)
}
fun getCheckoutUrl(productType: ProductType): String {
return checkouts[productType]!!.webUrl
}
suspend fun waitForCheckout(productType: ProductType) {
val result = shopifyService.checkout(true, checkouts[productType]!!.id)
result.onSuccess { checkout ->
if (checkout.order != null && checkout.lineItems != null) {
val event = ShopOrderToEventConverter().convert(checkout to productType)
Analytics.send(event)
}
}
}
}
private fun Storefront.MoneyV2.format(): String {
val currencySymbol = Currency.getInstance(currencyCode.name).symbol
val amountFormatted = BigDecimal(amount).setScale(2)
return currencySymbol + amountFormatted
}
private fun Storefront.Checkout.getAppliedDiscount(): String? {
val discountApplication =
discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication
return discountApplication?.code
}

View file

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

View file

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

View file

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

View file

@ -1,133 +0,0 @@
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)
}
@Suppress("MagicNumber")
private val allowedCardNetworks = JSONArray(
listOf(
"AMEX",
"DISCOVER",
"INTERAC",
"JCB",
"MASTERCARD",
"VISA",
),
)
private val allowedCardAuthMethods = JSONArray(
listOf(
"PAN_ONLY",
"CRYPTOGRAM_3DS",
),
)
private val merchantInfo: JSONObject = JSONObject().put("merchantName", "Example Merchant")
private fun gatewayTokenizationSpecification(merchantID: String): JSONObject {
return JSONObject().apply {
put("type", "PAYMENT_GATEWAY")
put(
"parameters",
JSONObject(
mapOf(
"gateway" to "shopify",
"gatewayMerchantId" to merchantID,
),
),
)
}
}
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)
}
}
fun getPaymentDataRequest(
price: String,
countryCode: String,
currencyCode: String,
merchantID: String,
): JSONObject? {
try {
return baseRequest.apply {
put("allowedPaymentMethods", JSONArray().put(cardPaymentMethod(merchantID)))
put("transactionInfo", getTransactionInfo(price, countryCode, currencyCode))
put("merchantInfo", merchantInfo)
// An optional shipping address requirement is a top-level property of the
// PaymentDataRequest JSON object.
val shippingAddressParameters = JSONObject().apply {
put("phoneNumberRequired", false)
// put("allowedCountryCodes", JSONArray(listOf("US", "GB")))
}
put("shippingAddressParameters", shippingAddressParameters)
put("shippingAddressRequired", true)
}
} catch (e: JSONException) {
return null
}
}
}
const val PAYMENTS_ENVIRONMENT = WalletConstants.ENVIRONMENT_TEST

View file

@ -1,267 +0,0 @@
package com.tangem.tap.common.shop.shopify
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.datasource.config.models.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
@Suppress("LargeClass")
class ShopifyService(private val application: Application, val shop: ShopifyShop) {
private val client: GraphClient by lazy { initClient() }
@Suppress("MagicNumber")
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::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>(
delay = 1,
timeUnit = TimeUnit.SECONDS,
) {
this.retryWhen { result ->
when (result) {
is GraphCallResult.Success -> {
val checkout = result.response.data?.node as? Checkout
checkout?.order == 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 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)
}
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()
}

View file

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

View file

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

View file

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

View file

@ -1,59 +0,0 @@
package com.tangem.tap.features.shop.data
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.ShopResponse
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.tap.features.shop.domain.ShopRepository
import com.tangem.tap.features.shop.domain.models.SalesProduct
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
import java.util.Locale
/**
* Default implementation of shop feature repository
*
* @property tangemTechApi TangemTech API
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
internal class DefaultShopRepository(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : ShopRepository {
private val salesProductConverter = SalesProductConverter()
override suspend fun isShopifyOrderingAvailable(): Boolean {
return runCatching(dispatchers.io) { tangemTechApi.getShopInfo(name = SHOPIFY_NAME) }
.fold(
onSuccess = ShopResponse::isOrderingAvailable,
onFailure = {
Timber.e("Server error. isShopifyOrderingAvailable returns default value (true)")
true
},
)
}
override suspend fun getSalesProductInfo(): List<SalesProduct> {
return withIOContext {
val salesInfo = tangemTechApi.getSalesInfo(locale = getLocaleName(), shops = SHOPIFY_NAME)
salesProductConverter.convert(salesInfo)
}
}
private fun getLocaleName(): String {
return if (Locale.getDefault().language == "ru") {
RU_LOCALE
} else {
EN_LOCALE
}
}
private companion object {
private const val SHOPIFY_NAME = "shopify"
private const val RU_LOCALE = "ru"
private const val EN_LOCALE = "en"
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.tap.features.shop.data
import com.tangem.datasource.api.tangemTech.models.SalesResponse
import com.tangem.tap.features.shop.domain.models.Notification
import com.tangem.tap.features.shop.domain.models.ProductState
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.tap.features.shop.domain.models.SalesProduct
import com.tangem.utils.converter.Converter
internal class SalesProductConverter : Converter<SalesResponse, List<SalesProduct>> {
override fun convert(value: SalesResponse): List<SalesProduct> {
return value.sales.map { sales ->
val productState = when (sales.state) {
"order" -> ProductState.ORDER
"pre-order" -> ProductState.PRE_ORDER
"sold-out" -> ProductState.SOLD_OUT
else -> ProductState.SOLD_OUT
}
val productType = when (sales.product.code) {
"pack2" -> ProductType.WALLET_2_CARDS
"pack3" -> ProductType.WALLET_3_CARDS
else -> ProductType.WALLET_3_CARDS
}
SalesProduct(
id = sales.id,
productType = productType,
state = productState,
name = sales.product.name,
notification = sales.notification?.let { notification ->
Notification(
type = notification.type,
title = notification.title,
description = notification.description,
)
},
)
}
}
}

View file

@ -1,44 +0,0 @@
package com.tangem.tap.features.shop.di
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.tap.features.shop.data.DefaultShopRepository
import com.tangem.tap.features.shop.domain.DefaultShopifyOrderingAvailabilityUseCase
import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase
import com.tangem.tap.features.shop.domain.ShopRepository
import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
internal object ShopUseCaseModule {
@Provides
@ViewModelScoped
fun provideShopifyOrderingAvailabilityUseCase(shopRepository: ShopRepository): ShopifyOrderingAvailabilityUseCase {
return DefaultShopifyOrderingAvailabilityUseCase(
shopRepository = shopRepository,
)
}
@Provides
@ViewModelScoped
fun provideGetShopifySalesProductsUseCase(shopRepository: ShopRepository): GetShopifySalesProductsUseCase {
return GetShopifySalesProductsUseCase(
shopRepository = shopRepository,
)
}
@Provides
@ViewModelScoped
fun provideDefaultShopRepository(
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
): ShopRepository {
return DefaultShopRepository(tangemTechApi = tangemTechApi, dispatchers = dispatchers)
}
}

View file

@ -1,23 +0,0 @@
package com.tangem.tap.features.shop.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.tap.features.shop.toggles.DefaultShopifyFeatureToggleManager
import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ShopifyTogglesModule {
@Provides
@Singleton
fun provideDefaultShopifyFeatureToggleManager(
featureToggleManager: FeatureTogglesManager,
): ShopifyFeatureToggleManager {
return DefaultShopifyFeatureToggleManager(featureToggleManager)
}
}

View file

@ -1,15 +0,0 @@
package com.tangem.tap.features.shop.domain
/**
* Default implementation of use case to define shopify ordering availability
*
* @property shopRepository shop feature repository
*
[REDACTED_AUTHOR]
*/
internal class DefaultShopifyOrderingAvailabilityUseCase(
private val shopRepository: ShopRepository,
) : ShopifyOrderingAvailabilityUseCase {
override suspend fun invoke() = shopRepository.isShopifyOrderingAvailable()
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features.shop.domain
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.tap.features.shop.domain.models.SalesError
import com.tangem.tap.features.shop.domain.models.SalesProduct
/**
* Use case to get shopify available products
*
* @property shopRepository shop feature repository
*/
class GetShopifySalesProductsUseCase(
private val shopRepository: ShopRepository,
) {
suspend operator fun invoke(): Either<SalesError, List<SalesProduct>> {
return try {
shopRepository.getSalesProductInfo().right()
} catch (e: Exception) {
SalesError.DataError(e).left()
}
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.tap.features.shop.domain
import com.tangem.tap.features.shop.domain.models.SalesProduct
/**
* Shop feature repository
*
[REDACTED_AUTHOR]
*/
interface ShopRepository {
/** Get shopify ordering availability */
suspend fun isShopifyOrderingAvailable(): Boolean
/** Get actual sales product info */
suspend fun getSalesProductInfo(): List<SalesProduct>
}

View file

@ -1,12 +0,0 @@
package com.tangem.tap.features.shop.domain
/**
* Use case to define shopify ordering availability
*
[REDACTED_AUTHOR]
*/
internal interface ShopifyOrderingAvailabilityUseCase {
/** Get availability */
suspend operator fun invoke(): Boolean
}

View file

@ -1,22 +0,0 @@
package com.tangem.tap.features.shop.domain.models
private const val TANGEM_WALLET_2_CARDS_SKU = "TG115X2-S"
private const val TANGEM_WALLET_3_CARDS_SKU = "TG115X3-S"
enum class ProductType(val sku: String) {
WALLET_2_CARDS(TANGEM_WALLET_2_CARDS_SKU),
WALLET_3_CARDS(TANGEM_WALLET_3_CARDS_SKU),
;
companion object {
val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU)
fun fromSku(sku: String): ProductType? {
return when (sku) {
WALLET_2_CARDS.sku -> WALLET_2_CARDS
WALLET_3_CARDS.sku -> WALLET_3_CARDS
else -> null
}
}
}
}

View file

@ -1,5 +0,0 @@
package com.tangem.tap.features.shop.domain.models
sealed class SalesError {
data class DataError(val cause: Throwable) : SalesError()
}

View file

@ -1,30 +0,0 @@
package com.tangem.tap.features.shop.domain.models
/**
* Sales product
*
* @property id product id
* @property productType shows TW2 cards or 3 cards
* @property state state as order available etc
* @property name product name
* @property notification optional notification
*/
data class SalesProduct(
val id: String,
val productType: ProductType,
val state: ProductState,
val name: String,
val notification: Notification?,
)
data class Notification(
val type: String,
val title: String,
val description: String,
)
enum class ProductState {
ORDER,
SOLD_OUT,
PRE_ORDER,
}

View file

@ -1,60 +0,0 @@
package com.tangem.tap.features.shop.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase
import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Shop screen view model
*
* @property shopifyOrderingAvailabilityUseCase use case to define shopify ordering availability
* @property getShopifySalesProductsUseCase use case to get actual sales info
* @property dispatchers coroutine dispatchers provider
* @property appStateHolder redux state holder
*
[REDACTED_AUTHOR]
*/
@HiltViewModel
internal class ShopViewModel @Inject constructor(
private val shopifyOrderingAvailabilityUseCase: ShopifyOrderingAvailabilityUseCase,
private val getShopifySalesProductsUseCase: GetShopifySalesProductsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val appStateHolder: AppStateHolder,
) : ViewModel() {
/** Check ordering delay block visibility */
fun checkOrderingDelayBlockVisibility() {
viewModelScope.launch(dispatchers.main) {
val visibility = runCatching(dispatchers.io) { shopifyOrderingAvailabilityUseCase() }
.fold(onSuccess = { !it }, onFailure = { false })
appStateHolder.mainStore?.dispatch(action = ShopAction.SetOrderingDelayBlockVisibility(visibility))
}
}
/**
* Get actual sales products info
* to configure view dynamically
*/
fun getActualSalesInfo() {
viewModelScope.launch(dispatchers.main) {
val action = getShopifySalesProductsUseCase().fold(
ifLeft = {
ShopAction.SalesProductsError
},
ifRight = {
ShopAction.SalesProductsLoaded(it)
},
)
appStateHolder.mainStore?.dispatch(action)
}
}
}

View file

@ -1,52 +0,0 @@
package com.tangem.tap.features.shop.redux
import android.content.Intent
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.tap.common.shop.data.TangemProduct
import com.tangem.tap.common.shop.googlepay.GooglePayService
import com.tangem.tap.features.shop.domain.models.SalesProduct
import com.tangem.wallet.R
import org.rekotlin.Action
sealed interface ShopAction : Action {
object LoadProducts : ShopAction {
data class Success(val products: List<TangemProduct>) : ShopAction
object Failure : ShopAction, NotificationAction {
override val messageResource = R.string.common_server_unavailable
}
}
data class ApplyPromoCode(val promoCode: String) : ShopAction {
data class Success(val promoCode: String?, val products: List<TangemProduct>) : ShopAction
object InvalidPromoCode : ShopAction
}
object BuyWithGooglePay : ShopAction {
object UserCancelled : ShopAction
data class HandleGooglePayResponse(val resultCode: Int, val data: Intent?) : ShopAction
data class Failure(val exception: Throwable) : ShopAction
object Success : ShopAction
}
object StartWebCheckout : ShopAction
data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction {
object Success : ShopAction
object Failure : ShopAction
}
data class SelectProduct(val productType: ProductType) : ShopAction
object FinishSuccessfulOrder : ShopAction
object ResetState : ShopAction
data class SetOrderingDelayBlockVisibility(val visibility: Boolean) : ShopAction
data class SalesProductsLoaded(val salesProducts: List<SalesProduct>) : ShopAction
object SalesProductsError : ShopAction
}

View file

@ -1,128 +0,0 @@
package com.tangem.tap.features.shop.redux
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.common.analytics.events.Shop
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.scope
import com.tangem.tap.shopService
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
class ShopMiddleware {
val shopMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handle(action)
next(action)
}
}
}
}
@Suppress("LongMethod", "ComplexMethod")
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 {
shopService.getProducts().fold(
onSuccess = { store.dispatchOnMain(ShopAction.LoadProducts.Success(it)) },
onFailure = {
Timber.e(it)
store.dispatchOnMain(ShopAction.LoadProducts.Failure)
},
)
}
}
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 -> {
Analytics.send(Shop.Redirected(null))
store.dispatchOpenUrl(shopService.getCheckoutUrl(shopState.selectedProduct))
store.dispatch(ShopAction.FinishSuccessfulOrder)
}
is ShopAction.FinishSuccessfulOrder -> {
scope.launch {
shopService.waitForCheckout(shopState.selectedProduct)
}
}
else -> {}
}
}

View file

@ -1,49 +0,0 @@
package com.tangem.tap.features.shop.redux
import org.rekotlin.Action
object ShopReducer {
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)
is ShopAction.LoadProducts.Success -> state.copy(availableProducts = action.products)
is 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)
// TODO: change when we add support for GPay
is ShopAction.CheckIfGooglePayAvailable.Failure -> state.copy(isGooglePayAvailable = false)
is ShopAction.CheckIfGooglePayAvailable.Success -> state.copy(isGooglePayAvailable = false)
is ShopAction.ResetState -> ShopState()
is ShopAction.SetOrderingDelayBlockVisibility -> state.copy(isOrderingDelayBlockVisible = action.visibility)
is ShopAction.BuyWithGooglePay,
is ShopAction.LoadProducts,
is ShopAction.StartWebCheckout,
is ShopAction.CheckIfGooglePayAvailable,
is ShopAction.BuyWithGooglePay.Failure,
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse,
is ShopAction.BuyWithGooglePay.Success,
is ShopAction.BuyWithGooglePay.UserCancelled,
is ShopAction.FinishSuccessfulOrder,
is ShopAction.LoadProducts.Failure,
-> state
is ShopAction.SalesProductsLoaded -> state.copy(
salesProducts = action.salesProducts,
)
is ShopAction.SalesProductsError -> state.copy(
salesProducts = emptyList(),
)
}
}

View file

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

View file

@ -1,11 +0,0 @@
package com.tangem.tap.features.shop.toggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class DefaultShopifyFeatureToggleManager(
private val featureTogglesManager: FeatureTogglesManager,
) : ShopifyFeatureToggleManager {
override val isDynamicSalesProductsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("SHOPIFY_DYNAMIC_ENABLED")
}

View file

@ -1,10 +0,0 @@
package com.tangem.tap.features.shop.toggles
/**
* Shopify feature toggle manager that provides info about shopify toggle availability
*
*/
interface ShopifyFeatureToggleManager {
val isDynamicSalesProductsEnabled: Boolean
}

View file

@ -1,251 +0,0 @@
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 androidx.fragment.app.viewModels
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.common.GlobalLayoutStateHandler
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.shop.domain.models.ProductState
import com.tangem.tap.features.shop.domain.models.ProductType
import com.tangem.tap.features.shop.domain.models.SalesProduct
import com.tangem.tap.features.shop.presentation.ShopViewModel
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentShopBinding
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<ShopState> {
@Inject
lateinit var shopifyFeatureToggleManager: ShopifyFeatureToggleManager
private val binding: FragmentShopBinding by viewBinding(FragmentShopBinding::bind)
private var cardTranslationY = 70f
private lateinit var keyboardObserver: KeyboardObserver
private val viewModel by viewModels<ShopViewModel>()
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)
if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) {
viewModel.getActualSalesInfo()
} else {
viewModel.checkOrderingDelayBlockVisibility()
}
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(ShopAction.ResetState)
}
},
)
}
override fun onDestroyView() {
super.onDestroyView()
keyboardObserver.unregisterListener()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupCardsImages()
setupProductSelection()
setupPromoCodeEditText()
binding.toolbar.setNavigationOnClickListener {
requireActivity().onBackPressed()
}
keyboardObserver = KeyboardObserver(requireActivity()).apply {
registerListener { isVisible ->
binding.flCards.show(!isVisible)
}
}
}
@Suppress("MagicNumber")
private fun setupCardsImages() {
GlobalLayoutStateHandler(binding.imvSecond).apply {
onStateChanged = {
cardTranslationY = it.height * 0.15f
binding.imvSecond.animate()
.translationY(cardTranslationY)
.scaleX(0.9f)
.scaleY(0.9f)
.start()
binding.imvThird.animate()
.translationY(cardTranslationY * 2)
.scaleX(0.8f)
.scaleY(0.8f)
.start()
detach()
}
}
}
private fun setupProductSelection() = with(binding) {
chipProduct1.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_3_CARDS))
}
chipProduct2.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_2_CARDS))
}
chipProduct1.text = chipProduct1.getQuantityString(R.plurals.card_label_card_count, quantity = 3)
chipProduct2.text = chipProduct2.getQuantityString(R.plurals.card_label_card_count, quantity = 2)
}
private fun setupPromoCodeEditText() = with(binding) {
etPromoCode.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
}
etPromoCode.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
store.dispatch(ShopAction.ApplyPromoCode(etPromoCode.text.toString()))
}
}
}
override fun newState(state: ShopState) {
if (activity == null || view == null) return
animateProductSelection(state.selectedProduct)
handlePriceState(state)
handlePromoCodeState(state)
// TODO: https://tangem.slack.com/archives/C01HARKDLQ0/p1691421861756069
// if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) {
// handleNotificationBlock(state)
// } else {
// handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible)
// }
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) = with(binding) {
val translationY = if (show) cardTranslationY * 2 else cardTranslationY
if (show) imvThird.show()
imvThird.animate()
.translationY(translationY)
.setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
imvThird.show(show)
}
},
)
}
private fun handlePriceState(state: ShopState) = with(binding) {
tvTotal.text = state.total
tvTotalBeforeDiscount.text = state.priceBeforeDiscount
pbPrice.show(state.total == null)
}
private fun handlePromoCodeState(state: ShopState) = with(binding) {
if (state.promoCode == null && !etPromoCode.hasFocus()) {
etPromoCode.setText("")
}
pbPromoCode.show(state.promoCodeLoading)
}
// private fun handleOrderingDelayBlock(isVisible: Boolean) {
// if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide()
// }
//
// private fun handleNotificationBlock(state: ShopState) {
// if (isVisible) {
// binding.tvSoldOutDesc.show()
// getSelectedSalesProduct(state)?.notification?.let { notification ->
// binding.tvSoldOutDesc.text = notification.description
// }
// } else {
// binding.tvSoldOutDesc.hide()
// }
// }
private fun handleButtonsState(state: ShopState) = with(binding) {
btnPayGooglePay.root.show(state.isGooglePayAvailable)
btnAlternativePayment.show(state.isGooglePayAvailable)
btnMainAction.show(!state.isGooglePayAvailable)
if (state.total != null) {
btnAlternativePayment.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) }
btnPayGooglePay.root.setOnClickListener { store.dispatch(ShopAction.BuyWithGooglePay) }
btnMainAction.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) }
if (state.salesProducts.isNotEmpty()) {
getSelectedSalesProduct(state)?.let { selectedProduct ->
btnMainAction.text = getMainBtnTextByProductState(
productState = selectedProduct.state,
)
}
}
}
}
override fun handleOnBackPressed() {
store.dispatch(ShopAction.ResetState)
super.handleOnBackPressed()
}
private fun getSelectedSalesProduct(state: ShopState): SalesProduct? {
return state.salesProducts.find {
it.productType == state.selectedProduct
}
}
private fun getMainBtnTextByProductState(productState: ProductState): String = when (productState) {
ProductState.ORDER -> getString(R.string.shop_buy_now)
ProductState.SOLD_OUT -> "Sold out" // getString(R.string.sold_out) // todo finalize in next PR
ProductState.PRE_ORDER -> "Pre order" // getString(R.string.pre_order) // todo finalize in next PR
}
}

View file

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

View file

@ -102,7 +102,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
),
appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey,
amplitudeApiKey = configValues.amplitudeApiKey,
shopify = configValues.shopifyShop,
sprinklr = configValues.sprinklr,
walletConnectProjectId = configValues.walletConnectProjectId,
tangemComAuthorization = configValues.tangemComAuthorization,

View file

@ -14,7 +14,6 @@ data class Config(
val isTopUpEnabled: Boolean = false,
@Deprecated("Not relevant since version 3.23")
val isCreatingTwinCardsAllowed: Boolean = false,
val shopify: ShopifyShop? = null,
val sprinklr: SprinklrConfig? = null,
val walletConnectProjectId: String = "",
val tangemComAuthorization: String? = null,

View file

@ -31,7 +31,6 @@ class ConfigValueModel(
val blockcypherTokens: Set<String>?,
val infuraProjectId: String?,
val appsFlyer: AppsFlyer,
val shopifyShop: ShopifyShop?,
val sprinklr: SprinklrConfig?,
val tronGridApiKey: String,
val amplitudeApiKey: String,

View file

@ -1,14 +0,0 @@
package com.tangem.datasource.config.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ShopifyShop(
@Json(name = "domain")
val domain: String,
@Json(name = "storefrontApiKeyAndroid")
val storefrontApiKey: String,
@Json(name = "merchantID")
val merchantID: String?,
)

View file

@ -3,10 +3,6 @@
"name": "NEW_CARD_SCANNING_ENABLED",
"version": "undefined"
},
{
"name": "SHOPIFY_DYNAMIC_ENABLED",
"version": "undefined"
},
{
"name": "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED",
"version": "undefined"

View file

@ -11,7 +11,6 @@ data class NavigationState(
enum class AppScreen(val isDialogFragment: Boolean = false) {
Home,
Shop,
Disclaimer,
OnboardingNote,
OnboardingWallet,

View file

@ -61,7 +61,6 @@ okhttp = "4.9.3"
rekotlin = "1.0.4"
retrofit = "2.8.1"
retrofitMoshiConverter = "2.6.0"
shopifyBuySdk = "12.0.0"
spongycastleCryptoCore = "1.58.0.0"
timber = "4.7.1"
viewBindingDelegate = "1.5.9"
@ -224,7 +223,6 @@ moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" }
shopify-buy = { module = "com.shopify.mobilebuysdk:buy3", version.ref = "shopifyBuySdk" }
spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "spongycastleCryptoCore" }
reKotlin = { module = "org.rekotlin:rekotlin", version.ref = "rekotlin" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }