Updated on 2026-08-14

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,128 +0,0 @@
package com.tangem.tap.features.shop.redux
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.scope
import com.tangem.tap.shopService
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
class ShopMiddleware {
val shopMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handle(action)
next(action)
}
}
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handle(action: Action) {
val shopState = store.state.shopState
if (action is NavigationAction.NavigateTo && action.screen == AppScreen.Shop) {
store.dispatch(ShopAction.LoadProducts)
}
if (action !is ShopAction) return
when (action) {
is ShopAction.ApplyPromoCode -> {
scope.launch {
if (action.promoCode.isBlank() && shopState.promoCode == null) {
store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode)
return@launch
}
val result = shopService.applyPromoCode(action.promoCode)
result.onSuccess { products ->
store.dispatchOnMain(
ShopAction.ApplyPromoCode.Success(
promoCode = products.first { it.type == shopState.selectedProduct }.appliedDiscount,
products = products,
),
)
}
result.onFailure { store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode) }
}
}
ShopAction.BuyWithGooglePay -> {
shopService.buyWithGooglePay(shopState.selectedProduct)
// shopService.subscribeToGooglePayResult(productType = shopState.selectedProduct) { result ->
// result.onSuccess {
// store.dispatch(ShopAction.BuyWithGooglePay.Success)
// }
// result.onFailure { error ->
// if (error is TangemSdkError.UserCancelled) {
// store.dispatch(ShopAction.BuyWithGooglePay.UserCancelled)
// } else {
// store.dispatch(ShopAction.BuyWithGooglePay.Failure(error))
// }
// }
// }
}
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse -> {
scope.launch {
val result = shopService.handleGooglePayResult(
action.resultCode,
action.data,
shopState.selectedProduct,
)
result.onSuccess {
store.dispatchOnMain(ShopAction.BuyWithGooglePay.Success)
}
result.onFailure {
store.dispatchOnMain(ShopAction.BuyWithGooglePay.Failure(it))
}
}
}
ShopAction.LoadProducts -> {
scope.launch {
shopService.getProducts().fold(
onSuccess = { store.dispatchOnMain(ShopAction.LoadProducts.Success(it)) },
onFailure = {
Timber.e(it)
store.dispatchOnMain(ShopAction.LoadProducts.Failure)
},
)
}
}
is ShopAction.CheckIfGooglePayAvailable -> {
scope.launch {
val isAvailable =
shopService.checkIfGooglePayAvailable(action.googlePayService).getOrNull()
?: false
val newAction = if (isAvailable) {
ShopAction.CheckIfGooglePayAvailable.Success
} else {
ShopAction.CheckIfGooglePayAvailable.Failure
}
store.dispatchOnMain(newAction)
}
}
ShopAction.StartWebCheckout -> {
Analytics.send(Shop.Redirected(null))
store.dispatchOpenUrl(shopService.getCheckoutUrl(shopState.selectedProduct))
store.dispatch(ShopAction.FinishSuccessfulOrder)
}
is ShopAction.FinishSuccessfulOrder -> {
scope.launch {
shopService.waitForCheckout(shopState.selectedProduct)
}
}
else -> {}
}
}

View file

@ -1,49 +0,0 @@
package com.tangem.tap.features.shop.redux
import org.rekotlin.Action
object ShopReducer {
fun reduce(action: Action, state: ShopState): ShopState = internalReduce(action, state)
}
private fun internalReduce(action: Action, state: ShopState): ShopState {
if (action !is ShopAction) return state
return when (action) {
is ShopAction.ApplyPromoCode -> state.copy(promoCode = action.promoCode, promoCodeLoading = true)
is ShopAction.LoadProducts.Success -> state.copy(availableProducts = action.products)
is ShopAction.ApplyPromoCode.InvalidPromoCode -> state.copy(promoCode = null, promoCodeLoading = false)
is ShopAction.ApplyPromoCode.Success -> {
state.copy(
promoCode = action.promoCode,
availableProducts = action.products,
promoCodeLoading = false,
)
}
is ShopAction.SelectProduct -> state.copy(selectedProduct = action.productType)
// TODO: change when we add support for GPay
is ShopAction.CheckIfGooglePayAvailable.Failure -> state.copy(isGooglePayAvailable = false)
is ShopAction.CheckIfGooglePayAvailable.Success -> state.copy(isGooglePayAvailable = false)
is ShopAction.ResetState -> ShopState()
is ShopAction.SetOrderingDelayBlockVisibility -> state.copy(isOrderingDelayBlockVisible = action.visibility)
is ShopAction.BuyWithGooglePay,
is ShopAction.LoadProducts,
is ShopAction.StartWebCheckout,
is ShopAction.CheckIfGooglePayAvailable,
is ShopAction.BuyWithGooglePay.Failure,
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse,
is ShopAction.BuyWithGooglePay.Success,
is ShopAction.BuyWithGooglePay.UserCancelled,
is ShopAction.FinishSuccessfulOrder,
is ShopAction.LoadProducts.Failure,
-> state
is ShopAction.SalesProductsLoaded -> state.copy(
salesProducts = action.salesProducts,
)
is ShopAction.SalesProductsError -> state.copy(
salesProducts = emptyList(),
)
}
}

View file

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

View file

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

View file

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

View file

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