diff --git a/app/build.gradle b/app/build.gradle index 70f6e3df99..aa1b8e24b1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,7 +38,7 @@ android { } debug_beta { initWith release - debuggable false + debuggable true versionNameSuffix "-beta" applicationIdSuffix ".debug" buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"' @@ -78,8 +78,8 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' implementation 'com.tangem:blockchain:develop-64' - implementation 'com.tangem.tangem-sdk-kotlin:core:develop-131' - implementation 'com.tangem.tangem-sdk-kotlin:android:develop-131' + implementation 'com.tangem.tangem-sdk-kotlin:core:develop-132' + implementation 'com.tangem.tangem-sdk-kotlin:android:develop-132' // WebView implementation "androidx.browser:browser:1.3.0" @@ -129,6 +129,15 @@ dependencies { // animation implementation "com.airbnb.android:lottie:3.4.0" + // Shopify + implementation('com.shopify.mobilebuysdk:buy3:12.0.0') { + exclude group: "com.shopify.graphql.support" + exclude module: 'joda-time' + } + + // Google Pay + implementation 'com.google.android.gms:play-services-wallet:19.1.0' + testImplementation 'junit:junit:4.13.2' testImplementation "com.google.truth:truth:1.1.3" androidTestImplementation 'androidx.test.ext:junit:1.1.3' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f358215715..b38eacf816 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,6 +39,9 @@ tools:ignore="GoogleAppIndexingWarning" tools:replace="android:fullBackupContent"> + { + store.dispatch( + ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data) + ) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 1d84bd399c..74e957f6c3 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -12,6 +12,7 @@ import com.tangem.tap.common.images.PicassoHelper import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.shop.TangemShopService import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader @@ -37,6 +38,7 @@ val store = Store( lateinit var preferencesStorage: PreferencesStorage lateinit var currenciesRepository: CurrenciesRepository lateinit var walletConnectRepository: WalletConnectRepository +lateinit var shopService: TangemShopService class TapApplication : Application() { override fun onCreate() { @@ -72,7 +74,10 @@ class TapApplication : Application() { val localLoader = FeaturesLocalLoader(this, moshi) val remoteLoader = FeaturesRemoteLoader(moshi) val configManager = ConfigManager(localLoader, remoteLoader) - configManager.load { store.dispatch(GlobalAction.SetConfigManager(configManager)) } + configManager.load { + store.dispatch(GlobalAction.SetConfigManager(configManager)) + shopService = TangemShopService(this, configManager.config.shopify!!) + } val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi)) warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) } } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 30d8e1b18a..5fdd3a9279 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -19,6 +19,7 @@ import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCar import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment import com.tangem.tap.features.send.ui.SendFragment +import com.tangem.tap.features.shop.ui.ShopFragment import com.tangem.tap.features.tokens.ui.AddTokensFragment import com.tangem.tap.features.wallet.ui.WalletDetailsFragment import com.tangem.tap.features.wallet.ui.WalletFragment @@ -69,6 +70,7 @@ fun FragmentActivity.addOnBackPressedDispatcher( private fun fragmentFactory(screen: AppScreen): Fragment { return when (screen) { AppScreen.Home -> HomeFragment() + AppScreen.Shop -> ShopFragment() AppScreen.OnboardingNote -> OnboardingNoteFragment() AppScreen.OnboardingWallet -> OnboardingWalletFragment() AppScreen.OnboardingTwins -> TwinsCardsFragment() diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index e468ec2203..182461aa4c 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -68,9 +68,16 @@ fun View.hide(invokeBeforeStateChanged: (() -> Unit)? = null) { this.visibility = View.GONE } -fun View.makeInvisible() { - if (this.visibility == View.INVISIBLE) return - this.visibility = View.INVISIBLE +fun View.invisible(invisible: Boolean = true, invokeBeforeStateChanged: (() -> Unit)? = null) { + if (invisible) { + if (this.visibility == View.INVISIBLE) return + + invokeBeforeStateChanged?.invoke() + this.visibility = View.INVISIBLE + } else { + this.show(invokeBeforeStateChanged) + } + } fun Context.dpToPixels(dp: Int): Int = diff --git a/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt b/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt index 81be964cb1..50c90e1133 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt @@ -29,6 +29,10 @@ fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition()) TransitionManager.beginDelayedTransition(this, transition) } +fun View.beginDelayedTransition(transition: Transition = AutoTransition()) { + (this as? ViewGroup)?.beginDelayedTransition(transition) +} + fun ChipGroup.fitChipsByGroupWidth() { val layoutStateHandler = GlobalLayoutStateHandler(this) layoutStateHandler.onStateChanged = stateHandler@{ diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index d797ff9010..cbfa2565fb 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -11,6 +11,7 @@ import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOt 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.send.redux.reducers.SendScreenReducer +import com.tangem.tap.features.shop.redux.ShopReducer import com.tangem.tap.features.tokens.redux.TokensReducer import com.tangem.tap.features.wallet.redux.reducers.WalletReducer import org.rekotlin.Action @@ -32,7 +33,8 @@ fun appReducer(action: Action, state: AppState?): AppState { detailsState = DetailsReducer.reduce(action, state), disclaimerState = DisclaimerReducer.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), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index d3c6f3656b..719386cad9 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -23,6 +23,8 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWallet import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState import com.tangem.tap.features.send.redux.middlewares.SendMiddleware import com.tangem.tap.features.send.redux.states.SendState +import com.tangem.tap.features.shop.redux.ShopMiddleware +import com.tangem.tap.features.shop.redux.ShopState import com.tangem.tap.features.tokens.redux.TokensMiddleware import com.tangem.tap.features.tokens.redux.TokensState import com.tangem.tap.features.wallet.redux.WalletState @@ -44,6 +46,7 @@ data class AppState( val disclaimerState: DisclaimerState = DisclaimerState(), val tokensState: TokensState = TokensState(), val walletConnectState: WalletConnectState = WalletConnectState(), + val shopState: ShopState = ShopState(), ) : StateType { companion object { @@ -62,7 +65,8 @@ data class AppState( DisclaimerMiddleware().disclaimerMiddleware, TokensMiddleware().tokensMiddleware, WalletConnectMiddleware().walletConnectMiddleware, - BackupMiddleware().backupMiddleware + BackupMiddleware().backupMiddleware, + ShopMiddleware().shopMiddleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index 61e3d263da..6f68439f61 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -11,6 +11,7 @@ data class NavigationState( enum class AppScreen { Home, + Shop, Disclaimer, OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther, Wallet, WalletDetails, diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt new file mode 100644 index 0000000000..d428a9d197 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt @@ -0,0 +1,216 @@ +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.tap.common.shop.data.ProductType +import com.tangem.tap.common.shop.data.TangemProduct +import com.tangem.tap.common.shop.data.TotalSum +import com.tangem.tap.common.shop.shopify.ShopifyShop +import com.tangem.tap.common.shop.shopify.data.CheckoutItem +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import java.math.BigDecimal +import java.util.* + +class TangemShopService(application: Application, shopifyShop: ShopifyShop) { + + private val shopifyService = ShopifyService(application, shopifyShop) + + private lateinit var product: Storefront.Product + + private val checkouts = mutableMapOf() + private val variants = mutableMapOf() + + private lateinit var googlePayService: GooglePayService + + suspend fun getProducts(): Result> { + val result = shopifyService.getProducts() + + result.onSuccess { + product = it.first { + val variantsSku = it.variants.edges.map { it.node.sku } + variantsSku.contains(ProductType.WALLET_2_CARDS.sku) && variantsSku.contains( + ProductType.WALLET_3_CARDS.sku + ) + } + product.variants.edges.map { it.node } + .forEach { variant -> + if (variant.sku == ProductType.WALLET_2_CARDS.sku) { + variants[ProductType.WALLET_2_CARDS] = variant + } else if (variant.sku == ProductType.WALLET_3_CARDS.sku) { + variants[ProductType.WALLET_3_CARDS] = variant + } + } + + + 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)) + } + return Result.failure(result.exceptionOrNull()!!) + } + + 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 { + this.googlePayService = googlePayService + return googlePayService.checkIfGooglePayAvailable() + } + + fun buyWithGooglePay(productType: ProductType) { + val totalPrice = checkouts[productType]!!.totalPriceV2.amount + googlePayService.payWithGooglePay( + totalPriceCents = totalPrice, currencyCode = checkouts[productType]!!.currencyCode.name, + merchantID = shopifyService.shop.merchantID + ) + } + +// fun subscribeToGooglePayResult( +// productType: ProductType, +// resultCallback: (Result) -> Unit +// ) { +// googlePayService.responseCallback = { result -> +// result.onFailure { } +// result.onSuccess { +// completeTokenizedPayment(it, productType) +// } +// } +// } + + suspend fun handleGooglePayResult( + resultCode: Int, + data: Intent?, + productType: ProductType + ): Result { + 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 { + 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> { + val products = variants.keys + .map { coroutineScope { async { applyPromoCode(promoCode, it) } } } + .awaitAll() + .map { result -> result.getOrElse { return Result.failure(it) } } + + return Result.success(products) + } + + suspend fun applyPromoCode(promoCode: String, productType: ProductType): Result { + 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 + } + + companion object { + const val TANGEM_WALLET_2_CARDS_SKU = "TG115x2" + const val TANGEM_WALLET_3_CARDS_SKU = "TG115x3" + } +} + +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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt b/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt new file mode 100644 index 0000000000..89116f8096 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.common.shop.data + +import com.tangem.tap.common.shop.TangemShopService + +enum class ProductType(val sku: String) { + WALLET_2_CARDS(TangemShopService.TANGEM_WALLET_2_CARDS_SKU), + WALLET_3_CARDS(TangemShopService.TANGEM_WALLET_3_CARDS_SKU) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt new file mode 100644 index 0000000000..022af2b4ea --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt @@ -0,0 +1,7 @@ +package com.tangem.tap.common.shop.data + +data class TangemProduct( + val type: ProductType, + val totalSum: TotalSum? = null, + val appliedDiscount: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt new file mode 100644 index 0000000000..a9e920745f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.common.shop.data + +data class TotalSum( + val finalValue: String? = null, + val beforeDiscount: String? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt new file mode 100644 index 0000000000..13c3913ebe --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt @@ -0,0 +1,148 @@ +package com.tangem.tap.common.shop + +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 com.tangem.tap.common.shop.googlepay.GooglePayUtil +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) -> Unit)? = null + + suspend fun checkIfGooglePayAvailable(): Result { + + 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 { + 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 +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt new file mode 100644 index 0000000000..610ab10c9d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt @@ -0,0 +1,137 @@ +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) + } + + private fun gatewayTokenizationSpecification(merchantID: String): JSONObject { + return JSONObject().apply { + put("type", "PAYMENT_GATEWAY") + put( + "parameters", JSONObject( + mapOf( + "gateway" to "shopify", + "gatewayMerchantId" to merchantID + ) + ) + ) + } + } + + private val allowedCardNetworks = JSONArray( + listOf( + "AMEX", + "DISCOVER", + "INTERAC", + "JCB", + "MASTERCARD", + "VISA" + ) + ) + + private val allowedCardAuthMethods = JSONArray( + listOf( + "PAN_ONLY", + "CRYPTOGRAM_3DS" + ) + ) + + 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) + } + } + + private val merchantInfo: JSONObject = + JSONObject().put("merchantName", "Example Merchant") + + + 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 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt new file mode 100644 index 0000000000..da24d1337d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt @@ -0,0 +1,384 @@ +package com.tangem.tap.common.shop + +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.tap.common.shop.shopify.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 + + +class ShopifyService(private val application: Application, val shop: ShopifyShop) { + val client: GraphClient by lazy { initClient() } + + + suspend fun getShopName(): Result { + val query = query { rootQuery: QueryRootQuery -> + rootQuery + .shop { shopQuery: ShopQuery -> + shopQuery + .name() + } + } + return when (val result = queryAsync(query)) { + is GraphCallResult.Success -> { + val name = result.response.data!!.shop.name + Result.success(name) + } + is GraphCallResult.Failure -> { + Result.failure(result.error) + } + } + } + + suspend fun getProducts(collectionTitleFilter: String? = null): Result> { + val filter = collectionTitleFilter?.let { "title:\"$it\"" } + + val query = query { rootQuery: QueryRootQuery -> + rootQuery + .collections( + { arg -> arg.first(250).query(filter) }, + ) { collectionConnectionQuery -> + 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 { + + val query = query { rootQuery: QueryRootQuery -> + rootQuery + .node(checkoutID) { query -> + query.onCheckout { checkoutQuery -> + with(checkoutQuery) { + checkoutFieldsFragment() + } + } + } + } + val retryHandler = RetryHandler.build( + 1, TimeUnit.SECONDS + ) { + this.retryWhen { result -> + when (result) { + is GraphCallResult.Success -> { + val checkout = result.response.data?.node as? Checkout + checkout == 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, + checkoutID: ID? = null + ): Result { + + + val storefrontLineItems: MutableList = 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 { + 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 { + 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 updateAddress( + address: MailingAddress, + checkoutID: ID, + waitForShippingRates: Boolean + ): Result { + val input = MailingAddressInput() + .setAddress1(address.address1) + .setAddress2(address.address2) + .setCity(address.city) + .setCountry(address.country) + .setFirstName(address.firstName) + .setLastName(address.lastName) + .setPhone(address.phone) + .setProvince(address.province) + .setZip(address.zip) + + val query = mutation { mutationQuery: MutationQuery -> + mutationQuery + .checkoutShippingAddressUpdateV2( + input, checkoutID + ) { shippingAddressUpdatePayloadQuery: CheckoutShippingAddressUpdateV2PayloadQuery -> + shippingAddressUpdatePayloadQuery + .checkout { checkoutQuery: CheckoutQuery -> + checkoutQuery.checkoutFieldsFragment() + } + .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery -> + userErrorQuery + .field() + .message() + } + } + } + + return runCheckoutMutation(query) + } + + suspend fun updateEmail(email: String?, checkoutID: ID): Result { + val query = mutation { mutationQuery: MutationQuery -> + mutationQuery + .checkoutEmailUpdateV2( + checkoutID, email + ) { emailUpdatePayloadQuery: CheckoutEmailUpdateV2PayloadQuery -> + emailUpdatePayloadQuery + .checkout { checkoutQuery: CheckoutQuery -> + checkoutQuery.checkoutFieldsFragment() + } + .checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery -> + userErrorQuery + .field() + .message() + } + } + } + return runCheckoutMutation(query) + } + + suspend fun updateShippingRate(handle: String?, checkoutID: ID): Result { + val query = mutation { mutationQuery: MutationQuery -> + mutationQuery + .checkoutShippingLineUpdate( + checkoutID, handle + ) { shippingLineUpdatePayloadQuery: CheckoutShippingLineUpdatePayloadQuery -> + shippingLineUpdatePayloadQuery + .checkout { checkoutQuery: CheckoutQuery -> + checkoutQuery.checkoutFieldsFragment() + } + .checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery -> + userErrorQuery + .field() + .message() + } + } + } + return runCheckoutMutation(query) + } + + suspend fun completeWithTokenizedPayment( + payment: TokenizedPaymentInputV3, + checkoutID: ID + ): Result { + + 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) + } + + fun startGooglePaySession() { +// PaySession() + } + + private suspend fun runCheckoutMutation(mutation: MutationQuery): Result { + 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 + ): GraphCallResult = + withContext(Dispatchers.IO) { + suspendCoroutine { continuation -> + client.queryGraph(query).enqueue(retryHandler = retryHandler) { result -> + continuation.resume(result) + } + } + } + + private suspend fun queryAsync( + query: QueryRootQuery, + ): GraphCallResult = + withContext(Dispatchers.IO) { + suspendCoroutine { continuation -> + client.queryGraph(query).enqueue { result -> + continuation.resume(result) + } + } + } + + private suspend fun mutationQueryAsync(query: MutationQuery): GraphCallResult = + 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() + object GooglePayFailed : ShopifyError() + class UserError(val errorMessage: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyShop.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyShop.kt new file mode 100644 index 0000000000..5fde9c60ff --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyShop.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.common.shop.shopify + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ShopifyShop( + val domain: String, + val storefrontApiKey: String, + val merchantID: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt new file mode 100644 index 0000000000..849c1f33c3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt @@ -0,0 +1,134 @@ +package com.tangem.tap.common.shop.shopify.data + +import com.shopify.buy3.Storefront + +fun Storefront.CheckoutQuery.checkoutFieldsFragment() { + // id() + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt new file mode 100644 index 0000000000..5dc508df40 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.common.shop.shopify.data + +import com.shopify.graphql.support.ID + +data class CheckoutItem( + val id: ID, + val quantity: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt new file mode 100644 index 0000000000..572a7b68c7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.common.shop.shopify.data + +import com.shopify.buy3.Storefront + +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() + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt index 412537befb..4324891cae 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt @@ -2,6 +2,7 @@ package com.tangem.tap.domain.configurable.config import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.common.extensions.VoidCallback +import com.tangem.tap.common.shop.shopify.ShopifyShop import com.tangem.tap.domain.configurable.Loader /** @@ -16,7 +17,8 @@ data class Config( val isSendingToPayIdEnabled: Boolean = true, val isTopUpEnabled: Boolean = false, @Deprecated("Not relevant since version 3.23") - val isCreatingTwinCardsAllowed: Boolean = false + val isCreatingTwinCardsAllowed: Boolean = false, + val shopify: ShopifyShop? = null ) class ConfigManager( @@ -52,10 +54,10 @@ class ConfigManager( fun resetToDefault(name: String) { when (name) { isSendingToPayIdEnabled -> config = - config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled) + config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled) isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled) isCreatingTwinCardsAllowed -> config = - config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed) + config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed) } } @@ -87,6 +89,7 @@ class ConfigManager( infuraProjectId = values.infuraProjectId ), appsFlyerDevKey = values.appsFlyerDevKey, + shopify = values.shopifyShop, ) defaultConfig = defaultConfig.copy( coinMarketCapKey = values.coinMarketCapKey, @@ -99,6 +102,7 @@ class ConfigManager( infuraProjectId = values.infuraProjectId ), appsFlyerDevKey = values.appsFlyerDevKey, + shopify = values.shopifyShop, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt index edbbf6345c..c82d448e7d 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt @@ -1,5 +1,7 @@ package com.tangem.tap.domain.configurable.config +import com.tangem.tap.common.shop.shopify.ShopifyShop + /** [REDACTED_AUTHOR] */ @@ -20,6 +22,7 @@ class ConfigValueModel( val blockcypherTokens: Set?, val infuraProjectId: String?, val appsFlyerDevKey: String, + val shopifyShop: ShopifyShop? ) class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsConfirmFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsConfirmFragment.kt index 8e6dc116d8..07d0ea9b41 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsConfirmFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsConfirmFragment.kt @@ -68,7 +68,7 @@ class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm), } ConfirmScreenState.LongTap, ConfirmScreenState.AccessCode, ConfirmScreenState.PassCode -> { - toolbar.title = getString(R.string.details_row_title_manage_security) + toolbar.title = getString(R.string.details_manage_security_title) tv_warning_description.text = getString(R.string.details_security_management_warning) btn_confirm.text = getString(R.string.common_save_changes) btn_confirm.setCompoundDrawablesRelativeWithIntrinsicBounds( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index acb76789ba..8651dbd9d3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -125,7 +125,7 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber) { @@ -201,43 +205,40 @@ class AdditionalEmailInfo { } interface EmailData { - val subject: String - val mainMessage: String + val subjectResId: Int + val mainMessageResId: Int + + fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message fun prepare(infoHolder: AdditionalEmailInfo) {} - fun appendDelimiter(builder: StringBuilder) { - builder.append("----------\n") - } - - fun appendBlankLine(builder: StringBuilder) { - builder.append("\n") - } - fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String - fun joinTogether(infoHolder: AdditionalEmailInfo, allowToErasePhoneInfo: Boolean): String { - val allowToErasePhoneInfoDisclaimer = if (allowToErasePhoneInfo) { - "Following information is optional. You can erase it if you don’t want to share it.\n\n" - } else { - "" - } - return "$mainMessage\n\n\n\n" + - allowToErasePhoneInfoDisclaimer + - createOptionalMessage(infoHolder) + fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String { + return StringBuilder().apply { + append(context.getString(mainMessageResId)) + append("\n\n\n\n") + append(context.getString(getDataCollectionMessageResId())) + append(createOptionalMessage(infoHolder)) + }.toString() + } + + companion object { + internal fun StringBuilder.appendDelimiter() = append("----------\n") + internal fun StringBuilder.appendBlankLine() = append("\n") } } class RateCanBeBetterEmail : EmailData { - override val subject: String = "My suggestions" - override val mainMessage: String = "Tell us what functions you are missing, and we will try to help you." + override val subjectResId: Int = R.string.feedback_subject_rate_negative + override val mainMessageResId: Int = R.string.feedback_preface_rate_negative override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { val walletInfo = infoHolder.walletsInfo[0] return StringBuilder().apply { appendKeyValue("Card ID", infoHolder.cardId) appendKeyValue("Blockchain", walletInfo.blockchain.fullName) - appendBlankLine(this) + appendBlankLine() appendKeyValue("Phone model", infoHolder.phoneModel) appendKeyValue("OS version", infoHolder.osVersion) appendKeyValue("App version", infoHolder.appVersion) @@ -246,11 +247,21 @@ class RateCanBeBetterEmail : EmailData { } class ScanFailsEmail : EmailData { - override val subject: String = "Can’t scan a card" - override val mainMessage: String = "Please tell us what card do you have?" + + override val subjectResId: Int = R.string.feedback_subject_scan_failed + override val mainMessageResId: Int = R.string.feedback_preface_scan_failed + + override fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String { + return StringBuilder().apply { + append(context.getString(mainMessageResId)) + append("\n\n\n\n") + append(createOptionalMessage(infoHolder)) + }.toString() + } + override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { return StringBuilder().apply { - appendBlankLine(this) + appendBlankLine() appendKeyValue("Phone model", infoHolder.phoneModel) appendKeyValue("OS version", infoHolder.osVersion) appendKeyValue("App version", infoHolder.appVersion) @@ -259,8 +270,8 @@ class ScanFailsEmail : EmailData { } class SendTransactionFailedEmail(private val error: String) : EmailData { - override val subject: String = "Can’t send a transaction" - override val mainMessage: String = "Please tell us more about your issue. Every small detail can help." + override val subjectResId: Int = R.string.feedback_subject_tx_failed + override val mainMessageResId: Int = R.string.feedback_preface_tx_failed override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo() @@ -268,17 +279,17 @@ class SendTransactionFailedEmail(private val error: String) : EmailData { appendKeyValue("Card ID", infoHolder.cardId) appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion) appendKeyValue("Signed hashes", infoHolder.signedHashesCount) - appendDelimiter(this) + appendDelimiter() appendKeyValue("Blockchain", walletInfo.blockchain.fullName) appendKeyValue("Host", walletInfo.host) appendKeyValue("Token", infoHolder.token) appendKeyValue("Error", error) - appendDelimiter(this) + appendDelimiter() appendKeyValue("Source address", walletInfo.address) appendKeyValue("Destination address", infoHolder.destinationAddress) appendKeyValue("Amount", infoHolder.amount) appendKeyValue("Fee", infoHolder.fee) - appendBlankLine(this) + appendBlankLine() appendKeyValue("Phone model", infoHolder.phoneModel) appendKeyValue("OS version", infoHolder.osVersion) appendKeyValue("App version", infoHolder.appVersion) @@ -288,16 +299,16 @@ class SendTransactionFailedEmail(private val error: String) : EmailData { } class FeedbackEmail : EmailData { - override val subject: String + override val subjectResId: Int get() = if (isS2CCard) s2cSubject else tangemSubject - override val mainMessage: String + override val mainMessageResId: Int get() = if (isS2CCard) s2cMainMessage else tangemMainMessage - private val tangemSubject = "Tangem feedback" - private val tangemMainMessage = "Hi support team," + private val tangemSubject: Int = R.string.feedback_subject_support_tangem + private val tangemMainMessage: Int = R.string.feedback_preface_support - private val s2cSubject = "Feedback" - private val s2cMainMessage = "Hi support team," + private val s2cSubject: Int = R.string.feedback_subject_support + private val s2cMainMessage: Int = R.string.feedback_preface_support private var isS2CCard = false @@ -312,26 +323,26 @@ class FeedbackEmail : EmailData { builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount) infoHolder.walletsInfo.forEach { - appendDelimiter(builder) + builder.appendDelimiter() builder.appendKeyValue("Blockchain", it.blockchain.fullName) builder.appendKeyValue("Host", it.host) builder.appendKeyValue("Wallet address", it.address) builder.appendKeyValue("Explorer link", it.explorerLink) } - appendBlankLine(builder) + builder.appendBlankLine() infoHolder.tokens.forEach { tokens -> - appendDelimiter(builder) + builder.appendDelimiter() builder.appendKeyValue("Blockchain", tokens.key.fullName) builder.appendKeyValue("Tokens", tokens.value.map { "${it.name} - ${it.symbol}" }.toString()) } - appendDelimiter(builder) - appendBlankLine(builder) - + builder.appendDelimiter() + builder.appendBlankLine() // appendKeyValue("Outputs count", infoHolder.outputsCount) builder.appendKeyValue("Phone model", infoHolder.phoneModel) builder.appendKeyValue("OS version", infoHolder.osVersion) builder.appendKeyValue("App version", infoHolder.appVersion) + return builder.toString() } } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index fb59fee5b6..95eada77e7 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -4,7 +4,6 @@ import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.AnalyticsParam import com.tangem.tap.common.analytics.GetCardSourceParams import com.tangem.tap.common.entities.IndeterminateProgressButton -import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.onCardScanned import com.tangem.tap.common.extensions.withMainContext import com.tangem.tap.common.post @@ -50,7 +49,7 @@ private val homeMiddleware: Middleware = { dispatch, state -> } is HomeAction.ReadCard -> handleReadCard() is HomeAction.GoToShop -> { - store.dispatchOpenUrl(HomeMiddleware.CARD_SHOP_URI) + store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop)) store.state.globalState.analyticsHandlers?.triggerEvent( event = AnalyticsEvent.GET_CARD, params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt index c747e1a745..882158b731 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt @@ -45,7 +45,7 @@ class AddressInfoBottomSheetDialog( btn_fl_share.setOnClickListener { store.dispatchShare(data.shareUrl) } - tv_recieve_message.text = getQRReceiveMessage(tv_recieve_message.context, stateDialog.currency) + tv_receive_message.text = getQRReceiveMessage(tv_receive_message.context, stateDialog.currency) } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 398ee44b55..d63d66047c 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -32,7 +32,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), StoreSubscriber, FragmentOnBackPressedHandler { private var accessCodeDialog: AccessCodeDialog? = null - private lateinit var cardsWidget: BackupCardsWidget + private lateinit var cardsWidget: WalletCardsWidget override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -48,7 +48,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), super.onViewCreated(view, savedInstanceState) val leapfrog = LeapfrogWidget(fl_cards_container) - cardsWidget = BackupCardsWidget(leapfrog) { 200f } + cardsWidget = WalletCardsWidget(leapfrog) { 200f } startPostponedEnterTransition() view_pager_backup_info.adapter = BackupInfoAdapter() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt similarity index 99% rename from app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt rename to app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt index 1b4ecba2a7..0efdbf3d0b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt @@ -10,7 +10,7 @@ import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapView import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapViewState import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget -class BackupCardsWidget( +class WalletCardsWidget( val leapfrogWidget: LeapfrogWidget, val getTopOfAnchorViewForActivateState: () -> Float, ) { 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 new file mode 100644 index 0000000000..9f7e7937b4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt @@ -0,0 +1,38 @@ +package com.tangem.tap.features.shop.redux + +import android.content.Intent +import com.tangem.tap.common.shop.GooglePayService +import com.tangem.tap.common.shop.data.ProductType +import com.tangem.tap.common.shop.data.TangemProduct +import org.rekotlin.Action + +sealed class ShopAction : Action { + + object LoadProducts : ShopAction() { + data class Success(val products: List) : 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() + + 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 ResetState : ShopAction() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt new file mode 100644 index 0000000000..605d5fe9d2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt @@ -0,0 +1,116 @@ +package com.tangem.tap.features.shop.redux + +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.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +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 + +class ShopMiddleware { + + val shopMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + handle(action) + next(action) + } + } + } +} + +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 { + val result = shopService.getProducts() + result.onSuccess { + store.dispatchOnMain(ShopAction.LoadProducts.Success(it)) + } + } + } + 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 -> { + store.dispatchOpenUrl(shopService.getCheckoutUrl(shopState.selectedProduct)) + } + } + +} \ 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 new file mode 100644 index 0000000000..8d4b4ff2a7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt @@ -0,0 +1,68 @@ +package com.tangem.tap.features.shop.redux + +import org.rekotlin.Action + +class ShopReducer { + companion object { + 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 + ) + 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.Success -> { + state.copy( + promoCode = action.promoCode, + availableProducts = action.products, + promoCodeLoading = false + + ) + } + 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.ResetState -> ShopState() + } +} \ 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 new file mode 100644 index 0000000000..a4ead917a3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.features.shop.redux + +import com.tangem.tap.common.shop.data.ProductType +import com.tangem.tap.common.shop.data.TangemProduct +import org.rekotlin.StateType + +data class ShopState( + val availableProducts: List = emptyList(), + val selectedProduct: ProductType = ProductType.WALLET_3_CARDS, + val promoCode: String? = null, + val promoCodeLoading: Boolean = false, + val isGooglePayAvailable: Boolean = false, // TODO: change when we add support for GPay +) : 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 + } +} 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 new file mode 100644 index 0000000000..27a20c0842 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt @@ -0,0 +1,167 @@ +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 com.tangem.tap.common.KeyboardObserver +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.redux.ShopAction +import com.tangem.tap.features.shop.redux.ShopState +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.fragment_shop.* +import org.rekotlin.StoreSubscriber + + +class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber { + + + 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) + activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + store.dispatch(NavigationAction.PopBackTo()) + store.dispatch(ShopAction.ResetState) + } + }) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupCardsImages() + setupProductSelection() + setupPromoCodeEditText() + + toolbar.setNavigationOnClickListener { + requireActivity().onBackPressed() + } + + + val keyboardObserver = KeyboardObserver(requireActivity()) + keyboardObserver.registerListener { isVisible -> + fl_cards.show(!isVisible) + } + } + + private fun setupCardsImages() { + imv_second.animate() + .translationY(70f) + .scaleX(0.9f) + .scaleY(0.9f) + .start() + imv_third.animate() + .translationY(140f) + .scaleX(0.8f) + .scaleY(0.8f) + .start() + } + + private fun setupProductSelection() { + chip_product_1.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_3_CARDS)) + } + chip_product_2.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_2_CARDS)) + } + } + + private fun setupPromoCodeEditText() { + et_promo_code.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 + } + + et_promo_code.onFocusChangeListener = OnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + store.dispatch(ShopAction.ApplyPromoCode(et_promo_code.text.toString())) + } + } + } + + override fun newState(state: ShopState) { + if (activity == null) return + + animateProductSelection(state.selectedProduct) + handlePriceState(state) + handlePromoCodeState(state) + 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) { + val translationY = if (show) 140f else 0f + if (show) imv_third.show() + imv_third.animate() + .translationY(translationY) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + imv_third?.show(show) + } + }) + } + + private fun handlePriceState(state: ShopState) { + tv_total.text = state.total + tv_total_before_discount.text = state.priceBeforeDiscount + + pb_price.show(state.total == null) + + } + + private fun handlePromoCodeState(state: ShopState) { + if (state.promoCode == null && !et_promo_code.hasFocus()) { + et_promo_code.setText("") + } + pb_promo_code.show(state.promoCodeLoading) + } + + private fun handleButtonsState(state: ShopState) { + btn_pay_google_pay.show(state.isGooglePayAvailable) + btn_alternative_payment.show(state.isGooglePayAvailable) + btn_main_action.show(!state.isGooglePayAvailable) + + if (state.total != null) { + btn_alternative_payment.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } + btn_main_action.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } + btn_pay_google_pay.setOnClickListener { store.dispatch(ShopAction.BuyWithGooglePay) } + } + } + + override fun handleOnBackPressed() { + store.dispatch(ShopAction.ResetState) + super.handleOnBackPressed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt index 178d8d514f..6a2559848e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -11,6 +11,7 @@ import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionInflater +import com.tangem.tap.common.extensions.getString import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.tokens.redux.TokensAction import com.tangem.tap.features.tokens.redux.TokensState @@ -96,7 +97,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), val menuItem = menu.findItem(R.id.menu_search) val searchView: SearchView = menuItem.actionView as SearchView - searchView.queryHint = "Type here to search" + searchView.queryHint = searchView.getString(R.string.add_token_search_hint) searchView.maxWidth = android.R.attr.width searchView.inputtedTextAsFlow() .debounce(400) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index bfd6057d73..f1d99a3053 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -23,8 +23,6 @@ import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.fragment_wallet_details.* -import kotlinx.android.synthetic.main.item_currency_wallet.view.* -import kotlinx.android.synthetic.main.item_popular_token.view.* import kotlinx.android.synthetic.main.layout_balance_error.* import kotlinx.android.synthetic.main.layout_balance_wallet_details.* import kotlinx.android.synthetic.main.layout_wallet_details.* @@ -197,7 +195,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS requireContext())) } iv_qr_code.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode()) - tv_recieve_message.text = getQRReceiveMessage(tv_recieve_message.context, state.currency) + tv_receive_message.text = getQRReceiveMessage(tv_receive_message.context, state.currency) } } diff --git a/app/src/main/res/color/selector_chip_shop.xml b/app/src/main/res/color/selector_chip_shop.xml new file mode 100644 index 0000000000..052f962892 --- /dev/null +++ b/app/src/main/res/color/selector_chip_shop.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_chip_shop_text.xml b/app/src/main/res/color/selector_chip_shop_text.xml new file mode 100644 index 0000000000..5d5afc33ff --- /dev/null +++ b/app/src/main/res/color/selector_chip_shop_text.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-de/buy_with_googlepay_button_content.xml b/app/src/main/res/drawable-de/buy_with_googlepay_button_content.xml new file mode 100755 index 0000000000..e1e2577110 --- /dev/null +++ b/app/src/main/res/drawable-de/buy_with_googlepay_button_content.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable-fr/buy_with_googlepay_button_content.xml b/app/src/main/res/drawable-fr/buy_with_googlepay_button_content.xml new file mode 100755 index 0000000000..4be47da569 --- /dev/null +++ b/app/src/main/res/drawable-fr/buy_with_googlepay_button_content.xml @@ -0,0 +1,54 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable-hdpi/card_placeholder_new.png b/app/src/main/res/drawable-hdpi/card_placeholder_new.png deleted file mode 100644 index 4c9cff9062..0000000000 Binary files a/app/src/main/res/drawable-hdpi/card_placeholder_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/card_placeholder_wallet.9.png b/app/src/main/res/drawable-hdpi/card_placeholder_wallet.9.png new file mode 100644 index 0000000000..97ea9bc260 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/card_placeholder_wallet.9.png differ diff --git a/app/src/main/res/drawable-it/buy_with_googlepay_button_content.xml b/app/src/main/res/drawable-it/buy_with_googlepay_button_content.xml new file mode 100755 index 0000000000..477793d6e5 --- /dev/null +++ b/app/src/main/res/drawable-it/buy_with_googlepay_button_content.xml @@ -0,0 +1,54 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable-ldpi/card_placeholder_new.png b/app/src/main/res/drawable-ldpi/card_placeholder_new.png deleted file mode 100644 index e9986ae27f..0000000000 Binary files a/app/src/main/res/drawable-ldpi/card_placeholder_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/card_placeholder_new.png b/app/src/main/res/drawable-mdpi/card_placeholder_new.png deleted file mode 100644 index b15e1cf3bb..0000000000 Binary files a/app/src/main/res/drawable-mdpi/card_placeholder_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/card_placeholder_wallet.9.png b/app/src/main/res/drawable-mdpi/card_placeholder_wallet.9.png new file mode 100644 index 0000000000..8a45ec562c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/card_placeholder_wallet.9.png differ diff --git a/app/src/main/res/drawable-ru/buy_with_googlepay_button_content.xml b/app/src/main/res/drawable-ru/buy_with_googlepay_button_content.xml new file mode 100755 index 0000000000..a624bdabf1 --- /dev/null +++ b/app/src/main/res/drawable-ru/buy_with_googlepay_button_content.xml @@ -0,0 +1,54 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable-xhdpi/card_placeholder_new.png b/app/src/main/res/drawable-xhdpi/card_placeholder_new.png deleted file mode 100644 index a9f3c750e0..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/card_placeholder_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/card_placeholder_wallet.9.png b/app/src/main/res/drawable-xhdpi/card_placeholder_wallet.9.png new file mode 100644 index 0000000000..ee8fdfd5f2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/card_placeholder_wallet.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png b/app/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png new file mode 100755 index 0000000000..40f767d533 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png differ diff --git a/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png b/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png new file mode 100755 index 0000000000..969cc52a94 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/card_placeholder_new.png b/app/src/main/res/drawable-xxhdpi/card_placeholder_new.png deleted file mode 100644 index 0caf7a0d35..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/card_placeholder_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/card_placeholder_wallet.9.png b/app/src/main/res/drawable-xxhdpi/card_placeholder_wallet.9.png new file mode 100644 index 0000000000..7483c7232b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_placeholder_wallet.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png b/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png new file mode 100755 index 0000000000..91035e2a41 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png differ diff --git a/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image.9.png b/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image.9.png new file mode 100755 index 0000000000..dda66dfd0d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image.9.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/card_placeholder_new.png b/app/src/main/res/drawable-xxxhdpi/card_placeholder_new.png deleted file mode 100644 index 56a6e580f0..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/card_placeholder_new.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/card_placeholder_wallet.9.png b/app/src/main/res/drawable-xxxhdpi/card_placeholder_wallet.9.png new file mode 100644 index 0000000000..3fb1bc1422 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/card_placeholder_wallet.9.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png b/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png new file mode 100755 index 0000000000..ab905289bc Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png b/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png new file mode 100755 index 0000000000..e67fdaf516 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png differ diff --git a/app/src/main/res/drawable/buy_with_googlepay_button_content.xml b/app/src/main/res/drawable/buy_with_googlepay_button_content.xml new file mode 100755 index 0000000000..a893cd0cac --- /dev/null +++ b/app/src/main/res/drawable/buy_with_googlepay_button_content.xml @@ -0,0 +1,54 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/googlepay_button_background.xml b/app/src/main/res/drawable/googlepay_button_background.xml new file mode 100755 index 0000000000..b81005b06f --- /dev/null +++ b/app/src/main/res/drawable/googlepay_button_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/googlepay_button_content.xml b/app/src/main/res/drawable/googlepay_button_content.xml new file mode 100755 index 0000000000..b2cdb1d52b --- /dev/null +++ b/app/src/main/res/drawable/googlepay_button_content.xml @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml b/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml new file mode 100755 index 0000000000..9543c4bcb6 --- /dev/null +++ b/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/googlepay_button_overlay.xml b/app/src/main/res/drawable/googlepay_button_overlay.xml new file mode 100755 index 0000000000..70c7a92271 --- /dev/null +++ b/app/src/main/res/drawable/googlepay_button_overlay.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_clear_24.xml b/app/src/main/res/drawable/ic_clear_24.xml new file mode 100644 index 0000000000..cd2a842a1f --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_promo_code.xml b/app/src/main/res/drawable/ic_promo_code.xml new file mode 100644 index 0000000000..149d59df21 --- /dev/null +++ b/app/src/main/res/drawable/ic_promo_code.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_shipping.xml b/app/src/main/res/drawable/ic_shipping.xml new file mode 100644 index 0000000000..b36ffe1bed --- /dev/null +++ b/app/src/main/res/drawable/ic_shipping.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/shape_line.xml b/app/src/main/res/drawable/shape_line.xml new file mode 100644 index 0000000000..cf7b6cfb0f --- /dev/null +++ b/app/src/main/res/drawable/shape_line.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rectangle_rounded_4.xml b/app/src/main/res/drawable/shape_rectangle_rounded_4.xml new file mode 100644 index 0000000000..b7d03c2907 --- /dev/null +++ b/app/src/main/res/drawable/shape_rectangle_rounded_4.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/buy_with_googlepay_button.xml b/app/src/main/res/layout/buy_with_googlepay_button.xml new file mode 100755 index 0000000000..2553181fd8 --- /dev/null +++ b/app/src/main/res/layout/buy_with_googlepay_button.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/main/res/layout/dialog_onboarding_address_info.xml b/app/src/main/res/layout/dialog_onboarding_address_info.xml index 55617e452f..ca04440067 100644 --- a/app/src/main/res/layout/dialog_onboarding_address_info.xml +++ b/app/src/main/res/layout/dialog_onboarding_address_info.xml @@ -24,7 +24,7 @@ app:layout_constraintTop_toBottomOf="@+id/pseudo_toolbar" /> + app:title="@string/add_tokens_title" /> @@ -51,7 +51,7 @@ android:layout_marginBottom="16dp" android:enabled="false" android:gravity="center" - android:text="Save changes" + android:text="@string/common_save_changes" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index b6475a8ac3..58ac038846 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -133,32 +133,39 @@ app:layout_constraintTop_toBottomOf="@id/tv_issuer" tools:text="48 hashes" /> - - - + app:layout_constraintTop_toBottomOf="@+id/tv_signed_hashes_title"> + + + + + + app:layout_constraintTop_toBottomOf="@id/ll_manage_security" /> + app:title="@string/details_manage_security_title" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_popular_token.xml b/app/src/main/res/layout/item_popular_token.xml index 3f03f0279f..6b8ad727ed 100644 --- a/app/src/main/res/layout/item_popular_token.xml +++ b/app/src/main/res/layout/item_popular_token.xml @@ -79,7 +79,7 @@ + android:layout_height="120dp"> + tools:src="@drawable/shape_circle" /> + tools:text="J" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_wallet_details.xml b/app/src/main/res/layout/layout_wallet_details.xml index 2532441b29..8e33a5cead 100644 --- a/app/src/main/res/layout/layout_wallet_details.xml +++ b/app/src/main/res/layout/layout_wallet_details.xml @@ -71,19 +71,15 @@ - + app:layout_constraintGuide_percent="0.40" /> + tools:text="139mrsJgyWnJjkljlkhiuojlkljkjljljlkjlkfsdkdsjflsdkjflsdkjfffffffljlkjkljlkjlkjkljky9BV" /> - - - - + app:iconEndPadding="0dp" + app:iconStartPadding="12dp" + app:layout_constraintEnd_toStartOf="@+id/btn_share" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/iv_qr_code" /> + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/btn_copy" + app:layout_constraintTop_toTopOf="@+id/btn_copy" /> diff --git a/app/src/main/res/layout/layout_wallet_short_buttons.xml b/app/src/main/res/layout/layout_wallet_short_buttons.xml index 61217164ee..09318739b7 100644 --- a/app/src/main/res/layout/layout_wallet_short_buttons.xml +++ b/app/src/main/res/layout/layout_wallet_short_buttons.xml @@ -15,7 +15,9 @@ android:layout_marginBottom="33dp" android:drawableTop="@drawable/ic_group_1243" android:gravity="bottom|center_horizontal" + android:paddingStart="0dp" android:paddingTop="7dp" + android:paddingEnd="0dp" android:text="@string/wallet_button_scan" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/btn_trade" diff --git a/app/src/main/res/layout/test_shopify_fragment.xml b/app/src/main/res/layout/test_shopify_fragment.xml new file mode 100644 index 0000000000..eff6a14884 --- /dev/null +++ b/app/src/main/res/layout/test_shopify_fragment.xml @@ -0,0 +1,87 @@ + + + + +