Updated on 2026-08-14
This commit is contained in:
parent
16f8451745
commit
00de15f8e0
15 changed files with 223 additions and 71 deletions
|
|
@ -0,0 +1,37 @@
|
|||
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.tap.features.shop.domain.ShopRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
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 (false)")
|
||||
false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SHOPIFY_NAME = "shopify"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
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.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(
|
||||
tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ShopifyOrderingAvailabilityUseCase {
|
||||
return DefaultShopifyOrderingAvailabilityUseCase(
|
||||
shopRepository = DefaultShopRepository(tangemTechApi, dispatchers),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
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()
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.tap.features.shop.domain
|
||||
|
||||
/**
|
||||
* Shop feature repository
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal interface ShopRepository {
|
||||
|
||||
/** Get shopify ordering availability */
|
||||
suspend fun isShopifyOrderingAvailable(): Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.tap.features.shop.presentation
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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 dispatchers coroutine dispatchers provider
|
||||
* @property appStateHolder redux state holder
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@HiltViewModel
|
||||
internal class ShopViewModel @Inject constructor(
|
||||
private val shopifyOrderingAvailabilityUseCase: ShopifyOrderingAvailabilityUseCase,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,38 +8,40 @@ import com.tangem.tap.common.shop.googlepay.GooglePayService
|
|||
import com.tangem.wallet.R
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class ShopAction : Action {
|
||||
sealed interface ShopAction : Action {
|
||||
|
||||
object LoadProducts : ShopAction() {
|
||||
data class Success(val products: List<TangemProduct>) : ShopAction()
|
||||
object Failure : ShopAction(), NotificationAction {
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
data class Failure(val exception: Throwable) : ShopAction
|
||||
object Success : ShopAction
|
||||
}
|
||||
|
||||
object StartWebCheckout : ShopAction()
|
||||
object StartWebCheckout : ShopAction
|
||||
|
||||
data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction() {
|
||||
object Success : ShopAction()
|
||||
object Failure : ShopAction()
|
||||
data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction {
|
||||
object Success : ShopAction
|
||||
object Failure : ShopAction
|
||||
}
|
||||
|
||||
data class SelectProduct(val productType: ProductType) : ShopAction()
|
||||
data class SelectProduct(val productType: ProductType) : ShopAction
|
||||
|
||||
object FinishSuccessfulOrder : ShopAction()
|
||||
object FinishSuccessfulOrder : ShopAction
|
||||
|
||||
object ResetState : ShopAction()
|
||||
object ResetState : ShopAction
|
||||
|
||||
data class SetOrderingDelayBlockVisibility(val visibility: Boolean) : ShopAction
|
||||
}
|
||||
|
|
@ -6,63 +6,38 @@ object ShopReducer {
|
|||
fun reduce(action: Action, state: ShopState): ShopState = internalReduce(action, state)
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
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,
|
||||
)
|
||||
ShopAction.BuyWithGooglePay -> state
|
||||
ShopAction.LoadProducts -> state
|
||||
is ShopAction.LoadProducts.Success -> {
|
||||
state.copy(
|
||||
availableProducts = action.products,
|
||||
)
|
||||
}
|
||||
ShopAction.StartWebCheckout -> state
|
||||
ShopAction.ApplyPromoCode.InvalidPromoCode -> state.copy(
|
||||
promoCode = null,
|
||||
promoCodeLoading = false,
|
||||
)
|
||||
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)
|
||||
|
||||
)
|
||||
}
|
||||
is ShopAction.SelectProduct -> {
|
||||
state.copy(
|
||||
selectedProduct = action.productType,
|
||||
)
|
||||
}
|
||||
is ShopAction.CheckIfGooglePayAvailable -> {
|
||||
state
|
||||
}
|
||||
ShopAction.CheckIfGooglePayAvailable.Failure -> {
|
||||
state.copy(isGooglePayAvailable = false)
|
||||
}
|
||||
ShopAction.CheckIfGooglePayAvailable.Success -> {
|
||||
state.copy(isGooglePayAvailable = false) // TODO: change when we add support for GPay
|
||||
}
|
||||
is ShopAction.BuyWithGooglePay.Failure -> {
|
||||
state
|
||||
}
|
||||
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse -> {
|
||||
state
|
||||
}
|
||||
ShopAction.BuyWithGooglePay.Success -> {
|
||||
state
|
||||
}
|
||||
ShopAction.BuyWithGooglePay.UserCancelled -> {
|
||||
state
|
||||
}
|
||||
ShopAction.FinishSuccessfulOrder -> state
|
||||
ShopAction.ResetState -> ShopState()
|
||||
ShopAction.LoadProducts.Failure -> state
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ data class ShopState(
|
|||
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
|
||||
|
|
|
|||
|
|
@ -9,28 +9,35 @@ 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.tap.common.GlobalLayoutStateHandler
|
||||
import com.tangem.tap.common.KeyboardObserver
|
||||
import com.tangem.tap.common.extensions.getQuantityString
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.common.shop.data.ProductType
|
||||
import com.tangem.tap.features.BaseStoreFragment
|
||||
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.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentShopBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<ShopState> {
|
||||
@AndroidEntryPoint
|
||||
internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<ShopState> {
|
||||
|
||||
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 ->
|
||||
|
|
@ -43,6 +50,8 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel.checkOrderingDelayBlockVisibility()
|
||||
|
||||
activity?.onBackPressedDispatcher?.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
|
|
@ -133,6 +142,7 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
|
|||
animateProductSelection(state.selectedProduct)
|
||||
handlePriceState(state)
|
||||
handlePromoCodeState(state)
|
||||
handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible)
|
||||
handleButtonsState(state)
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +183,10 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
|
|||
pbPromoCode.show(state.promoCodeLoading)
|
||||
}
|
||||
|
||||
private fun handleOrderingDelayBlock(isVisible: Boolean) {
|
||||
if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide()
|
||||
}
|
||||
|
||||
private fun handleButtonsState(state: ShopState) = with(binding) {
|
||||
btnPayGooglePay.root.show(state.isGooglePayAvailable)
|
||||
btnAlternativePayment.show(state.isGooglePayAvailable)
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@
|
|||
android:layout_marginTop="14dp"
|
||||
android:background="@drawable/shape_rectangle_rounded_4"
|
||||
android:padding="16dp"
|
||||
android:text="@string/shop_sold_out_description_prefix"
|
||||
android:text="@string/shop_sold_out_description"
|
||||
android:textColor="@color/text_tertiary"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -51,4 +51,7 @@ interface TangemTechApi {
|
|||
@Header("card_id") cardId: String,
|
||||
@Body startReferralBody: StartReferralBody,
|
||||
): ReferralResponse
|
||||
|
||||
@GET("shops")
|
||||
suspend fun getShopInfo(@Query(value = "name") name: String): ShopResponse
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
||||
/**
|
||||
* Shop response
|
||||
*
|
||||
* @property isOrderingAvailable ordering availability
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class ShopResponse(
|
||||
@Json(name = "canOrder") val isOrderingAvailable: Boolean,
|
||||
)
|
||||
|
|
@ -336,7 +336,7 @@
|
|||
<string name="shop_i_have_a_promo_code">У меня есть промо-код…</string>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="shop_other_payment_methods">Другие способы оплаты</string>
|
||||
<string name="shop_sold_out_description_prefix">Из-за большого количества заказов, которые мы получаем, cроки доставки могут быть увеличены.</string>
|
||||
<string name="shop_sold_out_description">Из-за высокого количества заказов, которые мы получаем, доставка может быть задержана на срок до 5 недель в зависимости от вашего местоположения</string>
|
||||
<string name="shop_total">Итого</string>
|
||||
<string name="solana_rent_warning">Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.</string>
|
||||
<string name="story_awe_description">Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.</string>
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@
|
|||
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="shop_other_payment_methods">Other payment methods</string>
|
||||
<string name="shop_sold_out_description_prefix">Due to the high volume of orders we are receiving, Shipping and Local Delivery orders may be delayed.</string>
|
||||
<string name="shop_sold_out_description">Due to the high volume of orders we are receiving shipping may be delayed up to 5 weeks depending on your location</string>
|
||||
<string name="shop_total">Total</string>
|
||||
<string name="solana_rent_warning">Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.</string>
|
||||
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue