diff --git a/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt new file mode 100644 index 0000000000..8f61a43c5f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt b/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt new file mode 100644 index 0000000000..2c00aa1181 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt @@ -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), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt new file mode 100644 index 0000000000..41bbe3d449 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt new file mode 100644 index 0000000000..c8cd6f3780 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt new file mode 100644 index 0000000000..5f6c351ce2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt b/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt new file mode 100644 index 0000000000..34966466cc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt @@ -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)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt index 219753c469..54c6bcad43 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt @@ -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) : ShopAction() - object Failure : ShopAction(), NotificationAction { + object LoadProducts : ShopAction { + data class Success(val products: List) : 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) : ShopAction() - object InvalidPromoCode : ShopAction() + data class ApplyPromoCode(val promoCode: String) : ShopAction { + data class Success(val promoCode: String?, val products: List) : 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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt index 8a97a1a58c..60cb0667ad 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt index ebbbb7cb79..491e961e58 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt index dccbe4d7b7..56ffe54b0f 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt @@ -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 { +@AndroidEntryPoint +internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber { private val binding: FragmentShopBinding by viewBinding(FragmentShopBinding::bind) private var cardTranslationY = 70f private lateinit var keyboardObserver: KeyboardObserver + private val viewModel by viewModels() + 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) diff --git a/app/src/main/res/layout/fragment_shop.xml b/app/src/main/res/layout/fragment_shop.xml index 523b68c535..0a61bf5b4a 100644 --- a/app/src/main/res/layout/fragment_shop.xml +++ b/app/src/main/res/layout/fragment_shop.xml @@ -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" /> diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 4125f7ed48..992f6d0eb5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -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 } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ShopResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ShopResponse.kt new file mode 100644 index 0000000000..6331011641 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ShopResponse.kt @@ -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, +) \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 2baace2c4d..e994de868f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -336,7 +336,7 @@ У меня есть промо-код… Tangem Wallet Другие способы оплаты - Из-за большого количества заказов, которые мы получаем, cроки доставки могут быть увеличены. + Из-за высокого количества заказов, которые мы получаем, доставка может быть задержана на срок до 5 недель в зависимости от вашего местоположения Итого Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index ea3c2c3c93..11ef0d6d75 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -332,7 +332,7 @@ I have a promo code… Tangem Wallet Other payment methods - Due to the high volume of orders we are receiving, Shipping and Local Delivery orders may be delayed. + Due to the high volume of orders we are receiving shipping may be delayed up to 5 weeks depending on your location Total 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. Store your crypto assets secure while keeping private keys contained in your card