Updated on 2026-08-14
This commit is contained in:
parent
996356ef61
commit
af4a278e88
43 changed files with 1 additions and 2286 deletions
|
|
@ -183,10 +183,6 @@ dependencies {
|
||||||
implementation(deps.kotsonGson)
|
implementation(deps.kotsonGson)
|
||||||
implementation(deps.spongecastle.core)
|
implementation(deps.spongecastle.core)
|
||||||
implementation(deps.lottie)
|
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.appCompatTheme)
|
||||||
implementation(deps.compose.accompanist.systemUiController)
|
implementation(deps.compose.accompanist.systemUiController)
|
||||||
implementation(deps.compose.accompanist.webView)
|
implementation(deps.compose.accompanist.webView)
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,6 @@ import com.tangem.tap.common.OnActivityResultCallback
|
||||||
import com.tangem.tap.common.SnackbarHandler
|
import com.tangem.tap.common.SnackbarHandler
|
||||||
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||||
import com.tangem.tap.common.redux.NotificationsHandler
|
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.TangemSdkManager
|
||||||
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
|
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
|
||||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
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.MainViewModel
|
||||||
import com.tangem.tap.features.main.model.Toast
|
import com.tangem.tap.features.main.model.Toast
|
||||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
|
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.features.welcome.ui.WelcomeFragment
|
||||||
import com.tangem.tap.proxy.AppStateHolder
|
import com.tangem.tap.proxy.AppStateHolder
|
||||||
import com.tangem.tap.proxy.redux.DaggerGraphAction
|
import com.tangem.tap.proxy.redux.DaggerGraphAction
|
||||||
|
|
@ -173,8 +169,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
initContent()
|
initContent()
|
||||||
|
|
||||||
checkGooglePayAvailability()
|
|
||||||
|
|
||||||
checkForNotificationPermission()
|
checkForNotificationPermission()
|
||||||
observeStateUpdates()
|
observeStateUpdates()
|
||||||
|
|
||||||
|
|
@ -256,14 +250,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkGooglePayAvailability() {
|
|
||||||
store.dispatch(
|
|
||||||
ShopAction.CheckIfGooglePayAvailable(
|
|
||||||
GooglePayService(createPaymentsClient(this), this),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode?> {
|
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode?> {
|
||||||
val tapApplication = application as TapApplication
|
val tapApplication = application as TapApplication
|
||||||
|
|
||||||
|
|
@ -377,13 +363,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
onActivityResultCallbacks.forEach { it(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) {
|
override fun addOnActivityResultCallback(callback: OnActivityResultCallback) {
|
||||||
|
|
@ -432,10 +411,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
|
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
|
||||||
val isNotScannedBefore = store.state.globalState.scanResponse == null
|
val isNotScannedBefore = store.state.globalState.scanResponse == null
|
||||||
val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted
|
val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted
|
||||||
val isShopNotOpened = store.state.shopState.total != null
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive && isShopNotOpened -> {
|
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> {
|
||||||
navigateToInitialScreen(intentWhichStartedActivity)
|
navigateToInitialScreen(intentWhichStartedActivity)
|
||||||
}
|
}
|
||||||
backStackIsEmpty -> {
|
backStackIsEmpty -> {
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ import com.tangem.tap.common.log.TimberFormatStrategy
|
||||||
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.warningMessage.WarningMessagesManager
|
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||||
import com.tangem.tap.domain.tasks.product.DerivationsFinder
|
import com.tangem.tap.domain.tasks.product.DerivationsFinder
|
||||||
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
|
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
|
||||||
|
|
@ -84,7 +83,6 @@ lateinit var foregroundActivityObserver: ForegroundActivityObserver
|
||||||
lateinit var activityResultCaller: ActivityResultCaller
|
lateinit var activityResultCaller: ActivityResultCaller
|
||||||
lateinit var preferencesStorage: PreferencesDataSource
|
lateinit var preferencesStorage: PreferencesDataSource
|
||||||
lateinit var walletConnectRepository: WalletConnectRepository
|
lateinit var walletConnectRepository: WalletConnectRepository
|
||||||
lateinit var shopService: TangemShopService
|
|
||||||
internal lateinit var derivationsFinder: DerivationsFinder
|
internal lateinit var derivationsFinder: DerivationsFinder
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
|
|
@ -300,7 +298,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initWithConfigDependency(config: Config) {
|
private fun initWithConfigDependency(config: Config) {
|
||||||
shopService = TangemShopService(this, config.shopify!!)
|
|
||||||
initAnalytics(this, config)
|
initAnalytics(this, config)
|
||||||
initFeedbackManager(this, foregroundActivityObserver, store)
|
initFeedbackManager(this, foregroundActivityObserver, store)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.onboarding.products.wallet.ui.OnboardingWalletFragment
|
||||||
import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
|
import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
|
||||||
import com.tangem.tap.features.send.ui.SendFragment
|
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.tokens.impl.presentation.TokensListFragment
|
||||||
import com.tangem.tap.features.welcome.ui.WelcomeFragment
|
import com.tangem.tap.features.welcome.ui.WelcomeFragment
|
||||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||||
|
|
@ -122,7 +121,6 @@ fun FragmentActivity.getPreviousScreen(): AppScreen? {
|
||||||
private fun fragmentFactory(screen: AppScreen): Fragment {
|
private fun fragmentFactory(screen: AppScreen): Fragment {
|
||||||
return when (screen) {
|
return when (screen) {
|
||||||
AppScreen.Home -> HomeFragment()
|
AppScreen.Home -> HomeFragment()
|
||||||
AppScreen.Shop -> ShopFragment()
|
|
||||||
AppScreen.OnboardingNote -> OnboardingNoteFragment()
|
AppScreen.OnboardingNote -> OnboardingNoteFragment()
|
||||||
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
|
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
|
||||||
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()
|
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()
|
||||||
|
|
|
||||||
|
|
@ -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.onboarding.products.wallet.redux.OnboardingWalletReducer
|
||||||
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
|
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
|
||||||
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
|
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.signin.redux.SignInReducer
|
||||||
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
|
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
|
||||||
import com.tangem.tap.features.welcome.redux.WelcomeReducer
|
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),
|
disclaimerState = DisclaimerReducer.reduce(action, state),
|
||||||
tokensState = TokensReducer.reduce(action, state),
|
tokensState = TokensReducer.reduce(action, state),
|
||||||
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
|
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
|
||||||
shopState = ShopReducer.reduce(action, state.shopState),
|
|
||||||
welcomeState = WelcomeReducer.reduce(action, state),
|
welcomeState = WelcomeReducer.reduce(action, state),
|
||||||
saveWalletState = SaveWalletReducer.reduce(action, state),
|
saveWalletState = SaveWalletReducer.reduce(action, state),
|
||||||
signInState = SignInReducer.reduce(action, state),
|
signInState = SignInReducer.reduce(action, state),
|
||||||
|
|
|
||||||
|
|
@ -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.saveWallet.redux.SaveWalletState
|
||||||
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
|
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
|
||||||
import com.tangem.tap.features.send.redux.states.SendState
|
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.SignInMiddleware
|
||||||
import com.tangem.tap.features.signin.redux.SignInState
|
import com.tangem.tap.features.signin.redux.SignInState
|
||||||
import com.tangem.tap.features.tokens.legacy.redux.TokensState
|
import com.tangem.tap.features.tokens.legacy.redux.TokensState
|
||||||
|
|
@ -56,7 +54,6 @@ data class AppState(
|
||||||
val disclaimerState: DisclaimerState = DisclaimerState(),
|
val disclaimerState: DisclaimerState = DisclaimerState(),
|
||||||
val tokensState: TokensState = TokensState(),
|
val tokensState: TokensState = TokensState(),
|
||||||
val walletConnectState: WalletConnectState = WalletConnectState(),
|
val walletConnectState: WalletConnectState = WalletConnectState(),
|
||||||
val shopState: ShopState = ShopState(),
|
|
||||||
val welcomeState: WelcomeState = WelcomeState(),
|
val welcomeState: WelcomeState = WelcomeState(),
|
||||||
val saveWalletState: SaveWalletState = SaveWalletState(),
|
val saveWalletState: SaveWalletState = SaveWalletState(),
|
||||||
val signInState: SignInState = SignInState(),
|
val signInState: SignInState = SignInState(),
|
||||||
|
|
@ -92,7 +89,6 @@ data class AppState(
|
||||||
DisclaimerMiddleware().disclaimerMiddleware,
|
DisclaimerMiddleware().disclaimerMiddleware,
|
||||||
WalletConnectMiddleware().walletConnectMiddleware,
|
WalletConnectMiddleware().walletConnectMiddleware,
|
||||||
BackupMiddleware().backupMiddleware,
|
BackupMiddleware().backupMiddleware,
|
||||||
ShopMiddleware().shopMiddleware,
|
|
||||||
WelcomeMiddleware().middleware,
|
WelcomeMiddleware().middleware,
|
||||||
SaveWalletMiddleware().middleware,
|
SaveWalletMiddleware().middleware,
|
||||||
LockUserWalletsTimerMiddleware().middleware,
|
LockUserWalletsTimerMiddleware().middleware,
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package com.tangem.tap.common.shop.data
|
|
||||||
|
|
||||||
data class TotalSum(
|
|
||||||
val finalValue: String? = null,
|
|
||||||
val beforeDiscount: String? = null,
|
|
||||||
)
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package com.tangem.tap.features.shop.domain.models
|
|
||||||
|
|
||||||
sealed class SalesError {
|
|
||||||
data class DataError(val cause: Throwable) : SalesError()
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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 -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -102,7 +102,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
|
||||||
),
|
),
|
||||||
appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey,
|
appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey,
|
||||||
amplitudeApiKey = configValues.amplitudeApiKey,
|
amplitudeApiKey = configValues.amplitudeApiKey,
|
||||||
shopify = configValues.shopifyShop,
|
|
||||||
sprinklr = configValues.sprinklr,
|
sprinklr = configValues.sprinklr,
|
||||||
walletConnectProjectId = configValues.walletConnectProjectId,
|
walletConnectProjectId = configValues.walletConnectProjectId,
|
||||||
tangemComAuthorization = configValues.tangemComAuthorization,
|
tangemComAuthorization = configValues.tangemComAuthorization,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ data class Config(
|
||||||
val isTopUpEnabled: Boolean = false,
|
val isTopUpEnabled: Boolean = false,
|
||||||
@Deprecated("Not relevant since version 3.23")
|
@Deprecated("Not relevant since version 3.23")
|
||||||
val isCreatingTwinCardsAllowed: Boolean = false,
|
val isCreatingTwinCardsAllowed: Boolean = false,
|
||||||
val shopify: ShopifyShop? = null,
|
|
||||||
val sprinklr: SprinklrConfig? = null,
|
val sprinklr: SprinklrConfig? = null,
|
||||||
val walletConnectProjectId: String = "",
|
val walletConnectProjectId: String = "",
|
||||||
val tangemComAuthorization: String? = null,
|
val tangemComAuthorization: String? = null,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ class ConfigValueModel(
|
||||||
val blockcypherTokens: Set<String>?,
|
val blockcypherTokens: Set<String>?,
|
||||||
val infuraProjectId: String?,
|
val infuraProjectId: String?,
|
||||||
val appsFlyer: AppsFlyer,
|
val appsFlyer: AppsFlyer,
|
||||||
val shopifyShop: ShopifyShop?,
|
|
||||||
val sprinklr: SprinklrConfig?,
|
val sprinklr: SprinklrConfig?,
|
||||||
val tronGridApiKey: String,
|
val tronGridApiKey: String,
|
||||||
val amplitudeApiKey: String,
|
val amplitudeApiKey: String,
|
||||||
|
|
|
||||||
|
|
@ -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?,
|
|
||||||
)
|
|
||||||
|
|
@ -3,10 +3,6 @@
|
||||||
"name": "NEW_CARD_SCANNING_ENABLED",
|
"name": "NEW_CARD_SCANNING_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "SHOPIFY_DYNAMIC_ENABLED",
|
|
||||||
"version": "undefined"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED",
|
"name": "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ data class NavigationState(
|
||||||
|
|
||||||
enum class AppScreen(val isDialogFragment: Boolean = false) {
|
enum class AppScreen(val isDialogFragment: Boolean = false) {
|
||||||
Home,
|
Home,
|
||||||
Shop,
|
|
||||||
Disclaimer,
|
Disclaimer,
|
||||||
OnboardingNote,
|
OnboardingNote,
|
||||||
OnboardingWallet,
|
OnboardingWallet,
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ okhttp = "4.9.3"
|
||||||
rekotlin = "1.0.4"
|
rekotlin = "1.0.4"
|
||||||
retrofit = "2.8.1"
|
retrofit = "2.8.1"
|
||||||
retrofitMoshiConverter = "2.6.0"
|
retrofitMoshiConverter = "2.6.0"
|
||||||
shopifyBuySdk = "12.0.0"
|
|
||||||
spongycastleCryptoCore = "1.58.0.0"
|
spongycastleCryptoCore = "1.58.0.0"
|
||||||
timber = "4.7.1"
|
timber = "4.7.1"
|
||||||
viewBindingDelegate = "1.5.9"
|
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" }
|
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
|
||||||
okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||||
okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" }
|
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" }
|
spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "spongycastleCryptoCore" }
|
||||||
reKotlin = { module = "org.rekotlin:rekotlin", version.ref = "rekotlin" }
|
reKotlin = { module = "org.rekotlin:rekotlin", version.ref = "rekotlin" }
|
||||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue