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/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 7f3bf094a7..56ce4dd062 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -15,6 +15,7 @@ import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInPro import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog +import com.tangem.tap.features.wallet.ui.dialogs.SimpleOkDialog import com.tangem.tap.store import com.tangem.wallet.R import org.rekotlin.StoreSubscriber @@ -48,6 +49,8 @@ class DialogManager : StoreSubscriber { if (dialog != null) return dialog = when (state.dialog) { + is AppDialog.SimpleOkDialog -> SimpleOkDialog.create(state.dialog, context) + is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context) is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context) is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context) is TwinCardsAction.Wallet.ShowInterruptDialog -> CreateWalletInterruptDialog.create(state.dialog, context) 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/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 3d33ceeaa8..f552e835f6 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -8,18 +8,26 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.amountToCreateAccount import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.topup.TradeCryptoHelper +import com.tangem.tap.features.demo.isDemoWallet import com.tangem.tap.features.wallet.redux.AddressData import com.tangem.tap.features.wallet.redux.reducers.createAddressesData import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.store +import timber.log.Timber /** [REDACTED_AUTHOR] */ suspend fun WalletManager.safeUpdate(): Result = try { - update() - Result.Success(wallet) + if (isDemoWallet()) { + Result.Success(wallet) + } else { + update() + Result.Success(wallet) + } } catch (exception: Exception) { + Timber.e(exception) + if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { Result.Failure(TapError.NoInternetConnection) } else { 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/StateDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt index bcde7dae3b..ecec5fb9f4 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt @@ -9,6 +9,8 @@ import com.tangem.tap.features.wallet.redux.Currency interface StateDialog sealed class AppDialog : StateDialog { + data class SimpleOkDialog(val header: String, val message: String) : AppDialog() + data class SimpleOkDialogRes(val headerId: Int, val messageId: Int) : AppDialog() object ScanFailsDialog : AppDialog() data class AddressInfoDialog( val currency: Currency, 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/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index cfd628a093..9e6fcd5de6 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -10,9 +10,11 @@ import com.tangem.tap.currenciesRepository import com.tangem.tap.domain.TapWorkarounds.isStart2Coin import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.domain.configurable.config.ConfigManager -import com.tangem.tap.domain.extensions.* +import com.tangem.tap.domain.extensions.makePrimaryWalletManager +import com.tangem.tap.domain.extensions.makeWalletManagersForApp import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.tokens.CardCurrencies +import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.NetworkConnectivity @@ -207,6 +209,9 @@ class TapWalletManager { data.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> { WalletAction.LoadData.Failure(TapError.UnknownBlockchain) } + data.isDemoCard() -> { + return null + } data.card.wallets.isEmpty() -> { WalletAction.EmptyWallet } 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/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt index 3cdf14dfea..663bbcbd6c 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt @@ -10,7 +10,7 @@ import com.tangem.wallet.R [REDACTED_AUTHOR] */ class WarningMessagesManager( - private val warningLoader: RemoteWarningLoader, + private val warningLoader: RemoteWarningLoader, ) { private val warningsList: MutableList = mutableListOf() @@ -33,15 +33,15 @@ class WarningMessagesManager( fun getWarnings(location: WarningMessage.Location, forBlockchains: List = emptyList()): List { return warningsList - .filter { !it.isHidden && it.location.contains(location) } - .filter { - val list = it.blockchainList - when { - list == null -> true - list.containsAny(forBlockchains) -> true - else -> false - } + .filter { !it.isHidden && it.location.contains(location) } + .filter { + val list = it.blockchainList + when { + list == null -> true + list.containsAny(forBlockchains) -> true + else -> false } + } } fun hideWarning(warning: WarningMessage): Boolean { @@ -49,7 +49,7 @@ class WarningMessagesManager( return when { foundWarning == null -> false foundWarning.type == WarningMessage.Type.Temporary - || foundWarning.type == WarningMessage.Type.AppRating -> { + || foundWarning.type == WarningMessage.Type.AppRating -> { if (foundWarning.isHidden) { false } else { @@ -80,32 +80,32 @@ class WarningMessagesManager( companion object { fun devCardWarning(): WarningMessage = WarningMessage( - "", - "", - type = WarningMessage.Type.Permanent, - priority = WarningMessage.Priority.Critical, - listOf(WarningMessage.Location.MainScreen), - null, - R.string.alert_title, - R.string.alert_developer_card, - WarningMessage.Origin.Local + "", + "", + type = WarningMessage.Type.Permanent, + priority = WarningMessage.Priority.Critical, + listOf(WarningMessage.Location.MainScreen), + null, + R.string.alert_title, + R.string.alert_developer_card, + WarningMessage.Origin.Local ) fun alreadySignedHashesWarning(): WarningMessage = WarningMessage( - "", - "", - type = WarningMessage.Type.Temporary, - priority = WarningMessage.Priority.Info, - listOf(WarningMessage.Location.MainScreen), - null, - R.string.alert_title, - R.string.alert_card_signed_transactions, - WarningMessage.Origin.Local + "", + "", + type = WarningMessage.Type.Temporary, + priority = WarningMessage.Priority.Info, + listOf(WarningMessage.Location.MainScreen), + null, + R.string.alert_title, + R.string.alert_card_signed_transactions, + WarningMessage.Origin.Local ) fun signedHashesMultiWalletWarning(): WarningMessage = WarningMessage( title = "", - message = "", + message = "", type = WarningMessage.Type.Temporary, priority = WarningMessage.Priority.Info, location = listOf(WarningMessage.Location.MainScreen), @@ -117,15 +117,15 @@ class WarningMessagesManager( ) fun appRatingWarning(): WarningMessage = WarningMessage( - "", - "", - WarningMessage.Type.AppRating, - WarningMessage.Priority.Info, - listOf(WarningMessage.Location.MainScreen), - null, - R.string.warning_rate_app_title, - R.string.warning_rate_app_message, - WarningMessage.Origin.Local + "", + "", + WarningMessage.Type.AppRating, + WarningMessage.Priority.Info, + listOf(WarningMessage.Location.MainScreen), + null, + R.string.warning_rate_app_title, + R.string.warning_rate_app_message, + WarningMessage.Origin.Local ) fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean { @@ -133,15 +133,15 @@ class WarningMessagesManager( } fun onlineVerificationFailed(): WarningMessage = WarningMessage( - "", - "", - type = WarningMessage.Type.Permanent, - priority = WarningMessage.Priority.Critical, - listOf(WarningMessage.Location.MainScreen), - null, - R.string.warning_failed_to_verify_card_title, - R.string.warning_failed_to_verify_card_message, - WarningMessage.Origin.Local + "", + "", + type = WarningMessage.Type.Permanent, + priority = WarningMessage.Priority.Critical, + listOf(WarningMessage.Location.MainScreen), + null, + R.string.warning_failed_to_verify_card_title, + R.string.warning_failed_to_verify_card_message, + WarningMessage.Origin.Local ) fun remainingSignaturesNotEnough(remainingSignatures: Int): WarningMessage = WarningMessage( @@ -169,6 +169,18 @@ class WarningMessagesManager( WarningMessage.Origin.Local ) + fun demoCardWarning(): WarningMessage = WarningMessage( + "", + "", + type = WarningMessage.Type.Permanent, + priority = WarningMessage.Priority.Critical, + listOf(WarningMessage.Location.MainScreen), + null, + R.string.alert_title, + R.string.alert_demo_message, + WarningMessage.Origin.Local + ) + const val REMAINING_SIGNATURES_WARNING = 10 } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index e75da7ca72..29ae9d45bc 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -5,8 +5,10 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.guard import com.tangem.common.extensions.toMapKey +import com.tangem.common.hdWallet.DerivationPath import com.tangem.operations.CommandResponse import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingTask @@ -16,10 +18,12 @@ import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletTask import com.tangem.tap.domain.ProductType import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain +import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.domain.tasks.product.CreateWalletsTask import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey import com.tangem.tap.domain.tasks.product.ProductCommandProcessor import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets +import com.tangem.tap.features.demo.DemoHelper data class CreateProductWalletTaskResponse( @@ -96,25 +100,27 @@ private class CreateWalletTangemNote : ProductCommandProcessor { + private lateinit var card: Card private var primaryCard: PrimaryCard? = null - private var createWalletResponse: CreateWalletResponse? = null override fun proceed( card: Card, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { + this.card = card val curves = card.getCurvesForNonCreatedWallets() + CreateWalletsTask(curves).run(session) { result -> when (result) { is CompletionResult.Success -> { - createWalletResponse = result.data.createWalletResponses[0] + val createWalletResponses = result.data.createWalletResponses when { card.settings.isBackupAllowed -> { - linkPrimaryCard(session, callback) + linkPrimaryCard(createWalletResponses, session, callback) } card.settings.isHDWalletAllowed -> { - deriveKeys(session, callback) + deriveKeys(createWalletResponses, session, callback) } else -> { callback( @@ -124,7 +130,6 @@ private class CreateWalletTangemWallet : ProductCommandProcessor callback(CompletionResult.Failure(result.error)) } @@ -132,6 +137,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { @@ -139,7 +145,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor { primaryCard = result.data - deriveKeys(session, callback) + deriveKeys(createWalletResponse, session, callback) } is CompletionResult.Failure -> { callback(CompletionResult.Failure(result.error)) @@ -149,28 +155,26 @@ private class CreateWalletTangemWallet : ProductCommandProcessor, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { - val derivationPaths = listOf(Blockchain.Bitcoin, Blockchain.Ethereum) - .mapNotNull { it.derivationPath() } - val response = createWalletResponse.guard { + val map = mutableMapOf>() + createWalletResponse.forEach { response -> + val blockchainsForCurve = getBlockchains(response.cardId).filter { + it.getSupportedCurves().contains(response.wallet.curve) + } + val derivationPaths = blockchainsForCurve.mapNotNull { it.derivationPath() } + if (derivationPaths.isNotEmpty()) { + map[response.wallet.publicKey.toMapKey()] = derivationPaths + } + } + if (map.isEmpty()) { callback(CompletionResult.Failure(TangemSdkError.UnknownError())) return } - if (derivationPaths.isNullOrEmpty()) { - callback( - CompletionResult.Success( - CreateProductWalletTaskResponse( - card = session.environment.card!!, primaryCard = primaryCard - ) - ) - ) - return - } - - DeriveMultipleWalletPublicKeysTask(mapOf(response.wallet.publicKey.toMapKey() to derivationPaths)) + DeriveMultipleWalletPublicKeysTask(map) .run(session) { result -> when (result) { is CompletionResult.Success -> { @@ -188,4 +192,12 @@ private class CreateWalletTangemWallet : ProductCommandProcessor { + return when { + DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains + card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet) + else -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt index f56e6f7bc8..114746a06f 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt @@ -13,9 +13,11 @@ import com.tangem.tap.common.extensions.appendIf import com.tangem.tap.common.extensions.readJsonFileToString import com.tangem.tap.domain.extensions.getCustomIconUrl import com.tangem.tap.domain.extensions.setCustomIconUrl +import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.network.createMoshi class CurrenciesRepository(val context: Application) { + private val moshi = createMoshi() private val blockchainsAdapter: JsonAdapter> = moshi.adapter( Types.newParameterizedType(List::class.java, Blockchain::class.java) @@ -28,10 +30,13 @@ class CurrenciesRepository(val context: Application) { ) fun loadCardCurrencies(cardId: String): CardCurrencies? { - val blockchains = loadSavedBlockchains(cardId) + val blockchains = loadSavedBlockchains(cardId).toMutableSet() + if (DemoHelper.isDemoCardId(cardId)) { + blockchains.addAll(DemoHelper.config.demoBlockchains) + } if (blockchains.isEmpty()) return null - return CardCurrencies(loadSavedTokens(cardId), blockchains) + return CardCurrencies(loadSavedTokens(cardId), blockchains.toList()) } fun saveCardCurrencies(cardId: String, currencies: CardCurrencies) { @@ -164,11 +169,10 @@ class CurrenciesRepository(val context: Application) { return excludeUnsupportedBlockchains(blockchains) } - //TODO: move to the App settings private fun excludeUnsupportedBlockchains(blockchains: List): List { return blockchains.toMutableList().apply { removeAll(listOf( - Blockchain.Fantom, Blockchain.FantomTestnet +// Blockchain.Fantom, Blockchain.FantomTestnet )) } } diff --git a/app/src/main/java/com/tangem/tap/domain/topup/TopUpManager.kt b/app/src/main/java/com/tangem/tap/domain/topup/TopUpManager.kt index 9dd6e91960..c5e74334a4 100644 --- a/app/src/main/java/com/tangem/tap/domain/topup/TopUpManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/topup/TopUpManager.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Token import com.tangem.blockchain.extensions.Result +import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner import com.tangem.tap.store @@ -14,7 +15,7 @@ import java.math.BigDecimal class TopUpManager { suspend fun topUpTestErc20Tokens(walletManager: EthereumWalletManager, token: Token) { - walletManager.update() + walletManager.safeUpdate() val amountToSend = Amount(walletManager.wallet.blockchain) val destinationAddress = token.contractAddress diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt index becb529e80..3a163149e5 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt @@ -16,6 +16,7 @@ import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget */ class TwinsCardWidget( val leapfrogWidget: LeapfrogWidget, + private val deviceScaleFactor: Float = 1f, val getTopOfAnchorViewForActivateState: () -> Float ) { @@ -87,20 +88,20 @@ class TwinsCardWidget( return when (cardNumber) { TwinCardNumber.First -> { TwinsCardProperties( - xTranslation = -120f, - yTranslation = -220f, + xTranslation = -120f * deviceScaleFactor, + yTranslation = -220f * deviceScaleFactor, rotation = -3f, elevation = 1f, - scale = 1f, + scale = deviceScaleFactor, ) } TwinCardNumber.Second -> { TwinsCardProperties( - xTranslation = 170f, - yTranslation = 170f, + xTranslation = 170f * deviceScaleFactor, + yTranslation = 170f * deviceScaleFactor, rotation = -3f, elevation = 0f, - scale = 1f, + scale = deviceScaleFactor, ) } } @@ -123,7 +124,7 @@ class TwinsCardWidget( xTranslation = twinProperties.xTranslation, yTranslation = twinProperties.yTranslation - topOfAnchorView, rotation = 0f, - scale = twinProperties.scale - 0.5f, + scale = (twinProperties.scale - 0.5f) * deviceScaleFactor, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index e56b4d9ce6..1d0dc4a9e0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -15,6 +15,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.crypto.CryptoUtils import com.tangem.operations.sign.SignHashCommand import com.tangem.tap.common.analytics.Analytics +import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.features.details.redux.walletconnect.* import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData @@ -35,18 +36,11 @@ class WalletConnectSdkHelper { id: Long, type: WcTransactionType, ): WcTransactionData? { - val walletManager = getWalletManager(session) ?: return null - try { - walletManager.update() - } catch (exception: Exception) { - Timber.e(exception) - return null - } + walletManager.safeUpdate() val wallet = walletManager.wallet - val balance = - wallet.amounts[AmountType.Coin]?.value ?: return null + val balance = wallet.amounts[AmountType.Coin]?.value ?: return null val gas = transaction.gas?.hexToBigDecimal() ?: transaction.gasLimit?.hexToBigDecimal() diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt new file mode 100644 index 0000000000..f80e04df6b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -0,0 +1,153 @@ +package com.tangem.tap.features.demo + +import com.tangem.blockchain.common.* +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.tap.common.extensions.dispatchNotification +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store +import com.tangem.wallet.BuildConfig +import com.tangem.wallet.R +import org.rekotlin.Action +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +interface DemoMiddleware { + fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean +} + +object DemoHelper { + val config = DemoConfig() + + private val demoMiddlewares = listOf( + DemoOnboardingNoteMiddleware(), + ) + + private val disabledActionFeatures = listOf( + WalletConnectAction.StartWalletConnect::class.java, + WalletAction.TradeCryptoAction.Buy::class.java, + WalletAction.TradeCryptoAction.Sell::class.java, + BackupAction.StartBackup::class.java, + WalletAction.ExploreAddress::class.java + ) + + fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId) + + fun isDemoCardId(cardId: String): Boolean = config.isDemoCardId(cardId) + + fun tryHandle(appState: () -> AppState?, action: Action): Boolean { + val scanResponse = getScanResponse(appState) ?: return false + if (!scanResponse.isDemoCard()) return false + + demoMiddlewares.forEach { + if (it.tryHandle(config, scanResponse, action)) return true + } + + disabledActionFeatures.firstOrNull { it == action::class.java }?.let { + store.dispatchNotification(R.string.alert_demo_feature_disabled) + return true + } + + return false + } + + fun injectDemoBalance(walletManager: WalletManager?) { + val manager = walletManager ?: return + + val blockchain = walletManager.wallet.blockchain + val amount = config.getBalance(blockchain) + manager.wallet.setAmount(amount) + } + + private fun getScanResponse(appState: () -> AppState?): ScanResponse? { + val state = appState() ?: return null + + return state.globalState.onboardingState.onboardingManager?.scanResponse + ?: state.globalState.scanResponse + } +} + +class DemoConfig { + + val demoBlockchains = listOf( + Blockchain.Bitcoin, + Blockchain.Ethereum, + Blockchain.Dogecoin, + Blockchain.Solana, + ) + + val demoCardIds: List by lazy { + val demoIds = (releaseDemoCardIds + testDemoCardIds).toMutableList() + if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds) + + return@lazy demoIds.distinct() + } + + private val walletBalances: Map = mapOf( + Blockchain.Bitcoin to Amount(0.028.toBigDecimal(), Blockchain.Bitcoin), + Blockchain.Ethereum to Amount(0.2311.toBigDecimal(), Blockchain.Ethereum), + Blockchain.Dogecoin to Amount(1450.025.toBigDecimal(), Blockchain.Dogecoin), + Blockchain.Solana to Amount(13.246.toBigDecimal(), Blockchain.Solana), + ) + + fun isDemoCardId(cardId: String): Boolean = demoCardIds.contains(cardId) + + fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy() + ?: Amount(BigDecimal.ZERO, blockchain).copy() + + private val releaseDemoCardIds = mutableListOf( + + ) + + private val testDemoCardIds = listOf( + "FB20000000000186", // Note ETH + "FB10000000000196", // Note BTC + "FB30000000000176", // Wallet + //TODO: delete bellow ids before 3.28 release + "AB01000000045060", // Note BTC + "AB02000000045028", // Note ETH + "AC79000000000004", // Wallet 4.46 + ) + + private val debugTestDemoCardIds = listOf( + "AB01000000045060", // Note BTC + "AB02000000045028", // Note ETH + "AC79000000000004", // Wallet 4.46 + ) +} + +class DemoTransactionSender( + private val walletManager: WalletManager, + private val sender: TransactionSender = walletManager as TransactionSender +) : TransactionSender { + + override suspend fun getFee(amount: Amount, destination: String): Result> = + sender.getFee(amount, destination) + + override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { + val dataToSign = randomString(32).toByteArray() + val signerResponse = signer.sign(dataToSign, walletManager.wallet.cardId, walletManager.wallet.publicKey) + return SimpleResult.Failure(Exception(ID)) + } + + private fun randomInt(from: Int, to: Int): Int = kotlin.random.Random.nextInt(from, to) + + private fun randomString(length: Int): String { + val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + return (1..length) + .map { randomInt(0, charPool.size) } + .map(charPool::get) + .joinToString("") + } + + companion object { + val ID = DemoTransactionSender::class.java.simpleName + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt new file mode 100644 index 0000000000..7537d54bdd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt @@ -0,0 +1,58 @@ +package com.tangem.tap.features.demo + +import com.tangem.common.extensions.guard +import com.tangem.tap.common.extensions.withMainContext +import com.tangem.tap.domain.extensions.makePrimaryWalletManager +import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction +import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.scope +import com.tangem.tap.store +import kotlinx.coroutines.launch +import org.rekotlin.Action + +/** +[REDACTED_AUTHOR] + */ +internal class DemoOnboardingNoteMiddleware : DemoMiddleware { + + override fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean { + val globalState = store.state.globalState + val noteState = store.state.onboardingNoteState + + when (action) { + is OnboardingNoteAction.Balance.Update -> { + val walletManager = if (noteState.walletManager != null) { + noteState.walletManager + } else { + val wmFactory = globalState.tapWalletManager.walletManagerFactory + val walletManager = wmFactory.makePrimaryWalletManager(scanResponse).guard { + return false + } + store.dispatch(OnboardingNoteAction.SetWalletManager(walletManager)) + walletManager + } + val balanceAmount = config.getBalance(walletManager.wallet.blockchain) + val loadedBalance = noteState.walletBalance.copy( + value = balanceAmount.value!!, + currency = Currency.Blockchain(walletManager.wallet.blockchain), + state = ProgressState.Done, + error = null, + criticalError = null + ) + walletManager.wallet.setAmount(balanceAmount) + + scope.launch { + withMainContext { + store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance)) + store.dispatch(OnboardingNoteAction.Balance.SetCriticalError(loadedBalance.criticalError)) + store.dispatch(OnboardingNoteAction.Balance.SetNonCriticalError(loadedBalance.error)) + } + } + return true + } + } + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt new file mode 100644 index 0000000000..f32c079efc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.features.demo + +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManager +import com.tangem.tap.domain.tasks.product.ScanResponse + +/** +[REDACTED_AUTHOR] + */ +fun ScanResponse.isDemoCard(): Boolean = DemoHelper.isDemoCardId(card.cardId) +fun WalletManager.isDemoWallet(): Boolean = DemoHelper.isDemoCardId(wallet.cardId) +fun Wallet.isDemoWallet(): Boolean = DemoHelper.isDemoCardId(cardId) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index a8517bd1bc..6db0bf1a97 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -17,9 +17,11 @@ import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.walletconnect.BnbHelper import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils +import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store import com.tangem.wallet.R +import org.rekotlin.Action import org.rekotlin.Middleware class WalletConnectMiddleware { @@ -28,138 +30,131 @@ class WalletConnectMiddleware { val walletConnectMiddleware: Middleware = { dispatch, state -> { next -> { action -> - when (action) { + handle(state, action) + next(action) + } + } + } - is WalletConnectAction.RestoreSessions -> { - walletConnectManager.restoreSessions() - } + private fun handle(state: () -> AppState?, action: Action) { + if (DemoHelper.tryHandle(state, action)) return - is WalletConnectAction.HandleDeepLink -> { - if (!action.wcUri.isNullOrBlank()) { - if (WalletConnectManager.isCorrectWcUri(action.wcUri)) { - store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri)) - } - } - } - - is WalletConnectAction.StartWalletConnect -> { - val uri = action.activity.getFromClipboard()?.toString() - if (uri != null && WalletConnectManager.isCorrectWcUri(uri)) { - store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) - } else { - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan)) - } - } - - is WalletConnectAction.ShowClipboardOrScanQrDialog -> { - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.ClipboardOrScanQr( - action.wcUri - ) - ) - ) - } - - is WalletConnectAction.OpeningSessionTimeout -> { - store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout)) - } - - is WalletConnectAction.FailureEstablishingSession -> { - if (action.session != null) { - walletConnectManager.disconnect(action.session) - } - } - - is WalletConnectAction.UnsupportedCard -> { - store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedCard)) - } - - is WalletConnectAction.OpenSession -> { - walletConnectManager.connect( - wcUri = action.wcUri, - ) - } - - is WalletConnectAction.RefuseOpeningSession -> { - store.dispatch( - GlobalAction.ShowDialog( - WalletConnectDialog.OpeningSessionRejected - ) - ) - } - - is WalletConnectAction.ScanCard -> { - scanCard(action.session, action.chainId) - } - - is WalletConnectAction.ApproveSession -> { - walletConnectManager.approve(action.session) - } - - is WalletConnectAction.DisconnectSession -> { - walletConnectManager.disconnect(action.session) - } - - is WalletConnectAction.HandleTransactionRequest -> { - walletConnectManager.handleTransactionRequest( - transaction = action.transaction, - session = action.session, - id = action.id, - type = action.type - ) - } - is WalletConnectAction.HandlePersonalSignRequest -> { - walletConnectManager.handlePersonalSignRequest( - message = action.message, - session = action.session, - id = action.id - ) - } - is WalletConnectAction.RejectRequest -> { - walletConnectManager.rejectRequest(action.session, action.id) - } - is WalletConnectAction.SendTransaction -> { - walletConnectManager.completeTransaction(action.session) - } - is WalletConnectAction.SignMessage -> { - walletConnectManager.sendSignedMessage(action.session) - } - is WalletConnectAction.BinanceTransaction.Trade -> { - val messageData = BnbHelper.createMessageData(action.order) - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.BnbTransactionDialog( - data = messageData, - session = action.sessionData.session, - sessionId = action.id, - cardId = action.sessionData.wallet.cardId, - dAppName = action.sessionData.peerMeta.name - ) - ) - ) - } - is WalletConnectAction.BinanceTransaction.Transfer -> { - val messageData = BnbHelper.createMessageData(action.order) - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.BnbTransactionDialog( - data = messageData, - session = action.sessionData.session, - sessionId = action.id, - cardId = action.sessionData.wallet.cardId, - dAppName = action.sessionData.peerMeta.name - ) - ) - ) - } - is WalletConnectAction.BinanceTransaction.Sign -> { - walletConnectManager.signBnb( - action.id, action.data, action.sessionData - ) + when (action) { + is WalletConnectAction.RestoreSessions -> { + walletConnectManager.restoreSessions() + } + is WalletConnectAction.HandleDeepLink -> { + if (!action.wcUri.isNullOrBlank()) { + if (WalletConnectManager.isCorrectWcUri(action.wcUri)) { + store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri)) } } - next(action) + } + is WalletConnectAction.StartWalletConnect -> { + val uri = action.activity.getFromClipboard()?.toString() + if (uri != null && WalletConnectManager.isCorrectWcUri(uri)) { + store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) + } else { + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan)) + } + } + is WalletConnectAction.ShowClipboardOrScanQrDialog -> { + store.dispatchOnMain( + GlobalAction.ShowDialog( + WalletConnectDialog.ClipboardOrScanQr( + action.wcUri + ) + ) + ) + } + is WalletConnectAction.OpeningSessionTimeout -> { + store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout)) + } + is WalletConnectAction.FailureEstablishingSession -> { + if (action.session != null) { + walletConnectManager.disconnect(action.session) + } + } + is WalletConnectAction.UnsupportedCard -> { + store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedCard)) + } + is WalletConnectAction.OpenSession -> { + walletConnectManager.connect( + wcUri = action.wcUri, + ) + } + is WalletConnectAction.RefuseOpeningSession -> { + store.dispatch( + GlobalAction.ShowDialog( + WalletConnectDialog.OpeningSessionRejected + ) + ) + } + is WalletConnectAction.ScanCard -> { + scanCard(action.session, action.chainId) + } + is WalletConnectAction.ApproveSession -> { + walletConnectManager.approve(action.session) + } + is WalletConnectAction.DisconnectSession -> { + walletConnectManager.disconnect(action.session) + } + is WalletConnectAction.HandleTransactionRequest -> { + walletConnectManager.handleTransactionRequest( + transaction = action.transaction, + session = action.session, + id = action.id, + type = action.type + ) + } + is WalletConnectAction.HandlePersonalSignRequest -> { + walletConnectManager.handlePersonalSignRequest( + message = action.message, + session = action.session, + id = action.id + ) + } + is WalletConnectAction.RejectRequest -> { + walletConnectManager.rejectRequest(action.session, action.id) + } + is WalletConnectAction.SendTransaction -> { + walletConnectManager.completeTransaction(action.session) + } + is WalletConnectAction.SignMessage -> { + walletConnectManager.sendSignedMessage(action.session) + } + is WalletConnectAction.BinanceTransaction.Trade -> { + val messageData = BnbHelper.createMessageData(action.order) + store.dispatchOnMain( + GlobalAction.ShowDialog( + WalletConnectDialog.BnbTransactionDialog( + data = messageData, + session = action.sessionData.session, + sessionId = action.id, + cardId = action.sessionData.wallet.cardId, + dAppName = action.sessionData.peerMeta.name + ) + ) + ) + } + is WalletConnectAction.BinanceTransaction.Transfer -> { + val messageData = BnbHelper.createMessageData(action.order) + store.dispatchOnMain( + GlobalAction.ShowDialog( + WalletConnectDialog.BnbTransactionDialog( + data = messageData, + session = action.sessionData.session, + sessionId = action.id, + cardId = action.sessionData.wallet.cardId, + dAppName = action.sessionData.peerMeta.name + ) + ) + ) + } + is WalletConnectAction.BinanceTransaction.Sign -> { + walletConnectManager.signBnb( + action.id, action.data, action.sessionData + ) } } } @@ -190,9 +185,9 @@ class WalletConnectMiddleware { } val walletManager = getWalletManager(scanResponse, blockchain).guard { - store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null)) - return - } + store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null)) + return + } val wallet = walletManager.wallet val derivedKey = @@ -227,7 +222,7 @@ class WalletConnectMiddleware { ): WalletManager? { val card = scanResponse.card val factory = store.state.globalState.tapWalletManager.walletManagerFactory - val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) { + val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) { Blockchain.EthereumTestnet } else { blockchain 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/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 6a8cc146b7..a611b435d6 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -13,6 +13,7 @@ import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.domain.extensions.makePrimaryWalletManager +import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.scope @@ -33,14 +34,16 @@ class OnboardingNoteMiddleware { private val onboardingNoteMiddleware: Middleware = { dispatch, state -> { next -> { action -> - handleNoteAction(action, dispatch) + handleNoteAction(state, action, dispatch) next(action) } } } -private fun handleNoteAction(action: Action, dispatch: DispatchFunction) { +private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch: DispatchFunction) { if (action !is OnboardingNoteAction) return + if (DemoHelper.tryHandle(appState, action)) return + val globalState = store.state.globalState val onboardingManager = globalState.onboardingState.onboardingManager ?: return diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index 12e87f2a70..c8482b0624 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.onboarding.products.twins.ui import android.os.Bundle +import android.util.TypedValue import android.view.View import android.view.animation.OvershootInterpolator import androidx.annotation.LayoutRes @@ -38,6 +39,7 @@ import kotlinx.android.synthetic.main.view_bg_twins_welcome.* import kotlinx.android.synthetic.main.view_onboarding_progress.* import kotlinx.android.synthetic.main.view_onboarding_tv_balance.* + class TwinsCardsFragment : BaseOnboardingFragment() { private var previousStep = TwinCardsStep.None @@ -75,7 +77,14 @@ class TwinsCardsFragment : BaseOnboardingFragment() { addBackPressHandler(this) reconfigureLayoutForTwins() - twinsWidget = TwinsCardWidget(LeapfrogWidget(cards_container)) { 285f } + + val typedValue = TypedValue() + resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true) + val deviceScaleFactorForWelcomeState = typedValue.float + + twinsWidget = TwinsCardWidget(LeapfrogWidget(cards_container), deviceScaleFactorForWelcomeState) { + 285f * deviceScaleFactorForWelcomeState + } btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container) toolbar.title = getText(R.string.twins_recreate_toolbar) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 29cdd561b2..9c61e0c402 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -16,6 +16,7 @@ import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware.Companion.BUY_WALLET_URL import com.tangem.tap.features.wallet.redux.Artwork @@ -164,14 +165,16 @@ class BackupMiddleware { val backupMiddleware: Middleware = { dispatch, state -> { next -> { action -> - if (action is BackupAction) handleBackupAction(action) + if (action is BackupAction) handleBackupAction(state, action) next(action) } } } } -private fun handleBackupAction(action: BackupAction) { +private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) { + if (DemoHelper.tryHandle(appState, action)) return + val backupState = store.state.onboardingWalletState.backupState val globalState = store.state.globalState 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..65d8a935a8 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 @@ -1,6 +1,7 @@ package com.tangem.tap.features.onboarding.products.wallet.ui import android.os.Bundle +import android.util.TypedValue import android.view.* import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity @@ -13,6 +14,7 @@ import com.squareup.picasso.Picasso import com.tangem.common.CardIdFormatter import com.tangem.common.core.CardIdDisplayFormat import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget +import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.PropertyCalculator import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.features.FragmentOnBackPressedHandler @@ -32,7 +34,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) @@ -47,8 +49,15 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val leapfrog = LeapfrogWidget(fl_cards_container) - cardsWidget = BackupCardsWidget(leapfrog) { 200f } + val typedValue = TypedValue() + resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true) + val deviceScaleFactor = typedValue.float + + val leapfrogCalculator = PropertyCalculator( + yTranslationFactor = 25f * deviceScaleFactor, + ) + val leapfrog = LeapfrogWidget(fl_cards_container, leapfrogCalculator) + cardsWidget = WalletCardsWidget(leapfrog, deviceScaleFactor) { 200f * deviceScaleFactor } startPostponedEnterTransition() view_pager_backup_info.adapter = BackupInfoAdapter() @@ -121,8 +130,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), tv_header.setText(R.string.onboarding_create_wallet_header) tv_body.setText(R.string.onboarding_create_wallet_body) - cardsWidget.toFolded() - startPostponedEnterTransition() + cardsWidget.toFolded(false) { startPostponedEnterTransition() } } @@ -379,7 +387,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), val shopMenuShouldBeVisible = (backupStep == BackupStep.ScanOriginCard || backupStep == BackupStep.AddBackupCards) && - backupState.buyAdditionalCardsUrl != null + backupState.buyAdditionalCardsUrl != null menu.getItem(0).isVisible = shopMenuShouldBeVisible } 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 86% 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..eb5ae173af 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,8 +10,9 @@ 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, + private val deviceScaleFactor: Float = 1f, val getTopOfAnchorViewForActivateState: () -> Float, ) { @@ -42,7 +43,7 @@ class BackupCardsWidget( createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)), createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP)) ) - leapfrogWidget.fold { animator.start() } + leapfrogWidget.fold(animate) { animator.start() } } fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) { @@ -70,7 +71,7 @@ class BackupCardsWidget( ) leapfrogWidget.fold { animator.doOnEnd { - leapfrogWidget.initViews() +// leapfrogWidget.initViews() leapfrogWidget.unfold() } animator.start() @@ -98,26 +99,26 @@ class BackupCardsWidget( private fun createWelcomeProperties(cardType: BackupCardType): CardProperties { return when (cardType) { BackupCardType.ORIGIN -> CardProperties( - xTranslation = 440f, - yTranslation = 0f, - rotation = 75f, - elevation = 2f, - scale = .9f, - ) + xTranslation = 440f * deviceScaleFactor, + yTranslation = 70f, + rotation = 75f, + elevation = 2f, + scale = .85f * deviceScaleFactor, + ) BackupCardType.FIRST_BACKUP -> CardProperties( - xTranslation = 10f, - yTranslation = -100f, - rotation = 100f, - elevation = 1f, - scale = .9f, -) + xTranslation = 10f * deviceScaleFactor, + yTranslation = -30f, + rotation = 100f, + elevation = 1f, + scale = .85f * deviceScaleFactor, + ) BackupCardType.SECOND_BACKUP -> CardProperties( - xTranslation = -440f, - yTranslation = -100f, - rotation = 70f, - elevation = 0f, - scale = .9f, - ) + xTranslation = -440f * deviceScaleFactor, + yTranslation = -30f, + rotation = 70f, + elevation = 0f, + scale = .85f * deviceScaleFactor, + ) } } @@ -125,24 +126,24 @@ class BackupCardsWidget( return when (cardType) { BackupCardType.ORIGIN -> CardProperties( xTranslation = 0f, - yTranslation = 0f, + yTranslation = 30f, rotation = 5f, elevation = 2f, - scale = 1f, + scale = 1f * deviceScaleFactor, ) BackupCardType.FIRST_BACKUP -> CardProperties( xTranslation = 0f, - yTranslation = -80f, + yTranslation = -50f, rotation = -5f, elevation = 1f, - scale = 0.9f, + scale = 0.9f * deviceScaleFactor, ) BackupCardType.SECOND_BACKUP -> CardProperties( xTranslation = 10f, - yTranslation = -170f, + yTranslation = -120f, rotation = -20f, elevation = 0f, - scale = 0.8f, + scale = 0.8f * deviceScaleFactor, ) } } @@ -154,21 +155,21 @@ class BackupCardsWidget( yTranslation = 0f, rotation = 0f, elevation = 2f, - scale = 1f, + scale = 1f * deviceScaleFactor, ) BackupCardType.FIRST_BACKUP -> CardProperties( xTranslation = 0f, yTranslation = 0f, rotation = 0f, elevation = 1f, - scale = 0.9f, + scale = 0.9f * deviceScaleFactor, ) BackupCardType.SECOND_BACKUP -> CardProperties( xTranslation = 0f, yTranslation = 0f, rotation = 0f, elevation = 0f, - scale = 0.8f, + scale = 0.8f * deviceScaleFactor, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 572658b793..34a63dd905 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -13,6 +13,7 @@ import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.AnalyticsParam import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.NavigationAction @@ -22,6 +23,8 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.domain.TapWorkarounds.isStart2Coin import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.extensions.minimalAmount +import com.tangem.tap.features.demo.DemoTransactionSender +import com.tangem.tap.features.demo.isDemoWallet import com.tangem.tap.features.send.redux.* import com.tangem.tap.features.send.redux.FeeAction.RequestFee import com.tangem.tap.features.send.redux.states.ButtonState @@ -32,6 +35,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdk +import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -164,9 +168,12 @@ private fun sendTransaction( ) ) } - val sendResult = try { - (walletManager as TransactionSender).send(txData, signer) + if (walletManager.isDemoWallet()) { + DemoTransactionSender(walletManager).send(txData, signer) + } else { + (walletManager as TransactionSender).send(txData, signer) + } } catch (ex: Exception) { FirebaseCrashlytics.getInstance().recordException(ex) delay(DELAY_SDK_DIALOG_CLOSE) @@ -238,6 +245,12 @@ private fun sendTransaction( message.contains("Target account is not created. To create account send 1+ XLM.") -> { dispatch(SendAction.SendError(TapError.XmlError.AssetAccountNotCreated)) } + message.contains(DemoTransactionSender.ID) -> { + store.dispatchDialogShow(AppDialog.SimpleOkDialogRes( + R.string.common_done, + R.string.alert_demo_tx_send + )) + } else -> { (sendResult.error as? TangemSdkError)?.let { error -> store.state.globalState.analyticsHandlers?.logCardSdkError( 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/models/PendingTransaction.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt index a449bb17ea..722b2c7d03 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt @@ -6,10 +6,12 @@ import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.common.Wallet import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.domain.extensions.toSendableAmounts +import java.math.BigDecimal data class PendingTransaction( val address: String?, - val amount: String?, + val amount: BigDecimal?, + val amountUi: String?, val currency: String, val type: PendingTransactionType ) @@ -38,6 +40,7 @@ fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransact return PendingTransaction( if (address == "unknown") null else address, + this.amount.value, this.amount.value?.toFormattedString(amount.decimals), this.amount.currencySymbol, type @@ -61,8 +64,12 @@ fun List.toPendingTransactionsForToken(token: Token, walletAddr return this.mapNotNull { it.toPendingTransactionForToken(token, walletAddress) } } -fun Wallet.getPendingTransactions(): List { - return recentTransactions.toPendingTransactions(address) +fun Wallet.getPendingTransactions(type: PendingTransactionType? = null): List { + val txs = recentTransactions.toPendingTransactions(address) + return when(type) { + null -> txs + else -> txs.filter { it.type == type } + } } fun Wallet.hasPendingTransactions(): Boolean { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index df4c9120f7..1d31def08c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -13,6 +13,10 @@ import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.currenciesRepository import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.extensions.makeWalletManagersForApp +import com.tangem.tap.features.demo.DemoHelper +import com.tangem.tap.features.demo.isDemoCard +import com.tangem.tap.features.wallet.models.PendingTransactionType +import com.tangem.tap.features.wallet.models.getPendingTransactions import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState @@ -22,6 +26,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.math.BigDecimal class MultiWalletMiddleware { fun handle( @@ -34,6 +39,9 @@ class MultiWalletMiddleware { is WalletAction.MultiWallet.AddWalletManagers -> { globalState.feedbackManager?.infoHolder?.setWalletsInfo(action.walletManagers) action.walletManagers.forEach { checkForRentWarning(it) } + if (globalState.scanResponse?.isDemoCard() == true) { + addDummyBalances(action.walletManagers) + } } is WalletAction.MultiWallet.SelectWallet -> { if (action.walletData != null) { @@ -99,28 +107,25 @@ class MultiWalletMiddleware { scope.launch { walletManagers.map { walletManager -> async(Dispatchers.IO) { - try { - walletManager.update() - val wallet = walletManager.wallet - val coinAmount = wallet.amounts[AmountType.Coin]?.value - if (coinAmount != null && !coinAmount.isZero()) { - scope.launch(Dispatchers.Main) { - if (walletState?.getWalletData(wallet.blockchain) == null) { - store.dispatch( - WalletAction.MultiWallet.AddWalletManagers( - listOfNotNull(walletManager) - ) + walletManager.safeUpdate() + val wallet = walletManager.wallet + val coinAmount = wallet.amounts[AmountType.Coin]?.value + if (coinAmount != null && !coinAmount.isZero()) { + scope.launch(Dispatchers.Main) { + if (walletState?.getWalletData(wallet.blockchain) == null) { + store.dispatch( + WalletAction.MultiWallet.AddWalletManagers( + listOfNotNull(walletManager) ) - store.dispatch( - WalletAction.MultiWallet.AddBlockchain( - wallet.blockchain - ) + ) + store.dispatch( + WalletAction.MultiWallet.AddBlockchain( + wallet.blockchain ) - store.dispatch(WalletAction.LoadWallet.Success(wallet)) - } + ) + store.dispatch(WalletAction.LoadWallet.Success(wallet)) } } - } catch (exception: Exception) { } } } @@ -167,6 +172,14 @@ class MultiWalletMiddleware { } } + private fun addDummyBalances(walletManagers: List) { + walletManagers.forEach { + if (it.wallet.fundsAvailable(AmountType.Coin) == BigDecimal.ZERO) { + DemoHelper.injectDemoBalance(it) + } + } + } + private fun addToken(token: Token, walletState: WalletState?, globalState: GlobalState?) { val scanResponse = globalState?.scanResponse ?: return @@ -206,13 +219,25 @@ class MultiWalletMiddleware { scope.launch { when (val result = rentProvider.minimalBalanceForRentExemption()) { is Result.Success -> { + fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean = balance >= rentExempt + + val balance = walletManager.wallet.fundsAvailable(AmountType.Coin) + val outgoingTxs = walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing) + val rentExempt = result.data + val show = if (outgoingTxs.isEmpty()) { + isNeedToShowWarning(balance, rentExempt) + } else { + val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO } + val rest = balance.minus(outgoingAmount) + isNeedToShowWarning(rest, rentExempt) + } + if (!show) return@launch + val currency = walletManager.wallet.blockchain.currency - val minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency") - val rentExempt = ("${result.data.stripZeroPlainString()} $currency") store.dispatchOnMain(WalletAction.SetWalletRent( blockchain = walletManager.wallet.blockchain, - minRent = minRent, - rentExempt = rentExempt + minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"), + rentExempt = ("${rentExempt.stripZeroPlainString()} $currency") )) } is Result.Failure -> {} diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 2cd5627545..fdf3312ab0 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -3,10 +3,12 @@ package com.tangem.tap.features.wallet.redux.middlewares import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.AmountType import com.tangem.tap.common.extensions.dispatchOnMain +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.domain.topup.TopUpManager import com.tangem.tap.domain.topup.TradeCryptoHelper +import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.send.redux.PrepareSendScreen import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.wallet.redux.Currency @@ -18,7 +20,9 @@ import timber.log.Timber class TradeCryptoMiddleware { - fun handle(action: WalletAction.TradeCryptoAction) { + fun handle(state: () -> AppState?, action: WalletAction.TradeCryptoAction) { + if (DemoHelper.tryHandle(state, action)) return + when (action) { is WalletAction.TradeCryptoAction.Buy -> startExchange(action) is WalletAction.TradeCryptoAction.Sell -> startExchange(action) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 37b71f1c2a..088fe72d40 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -3,13 +3,15 @@ package com.tangem.tap.features.wallet.redux.middlewares import android.content.Intent import android.net.Uri import androidx.core.content.ContextCompat -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.isZero import com.tangem.common.services.Result import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.tap.* import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppDialog @@ -18,10 +20,14 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.extensions.toSendableAmounts +import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.send.redux.PrepareSendScreen import com.tangem.tap.features.wallet.redux.* import com.tangem.tap.network.NetworkStateChanged +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch @@ -37,18 +43,20 @@ class WalletMiddleware { val walletMiddleware: Middleware = { dispatch, state -> { next -> { action -> - handleAction(action, dispatch) + handleAction(state, action, dispatch) next(action) } } } - private fun handleAction(action: Action, dispatch: DispatchFunction) { + private fun handleAction(state: () -> AppState?, action: Action, dispatch: DispatchFunction) { + if (DemoHelper.tryHandle(state, action)) return + val globalState = store.state.globalState val walletState = store.state.walletState when (action) { - is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(action) + is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action) is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState) is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState, globalState) is WalletAction.LoadWallet -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index 408f0ef8a3..b7c104861d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -17,6 +17,7 @@ import com.tangem.tap.domain.extensions.hasSignedHashes import com.tangem.tap.domain.extensions.remainingSignatures import com.tangem.tap.domain.isMultiwalletAllowed import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.preferencesStorage @@ -99,6 +100,9 @@ class WarningsMiddleware { addWarningMessage(WarningMessagesManager.onlineVerificationFailed()) } } + if (scanResponse.isDemoCard()){ + addWarningMessage(WarningMessagesManager.demoCardWarning()) + } setWarningMessages() } } 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/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt index 09c6094d38..31ab7c4368 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt @@ -62,7 +62,7 @@ class PendingTransactionsAdapter } view.tv_pending_transaction.text = view.context.getString(transactionDescriptionRes) - transaction.amount?.let { view.tv_pending_transaction_amount.text = "$it " } + transaction.amountUi?.let { view.tv_pending_transaction_amount.text = "$it " } view.tv_pending_transaction_currency.text = "${transaction.currency}" if (transaction.address != null) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt new file mode 100644 index 0000000000..aa15f94e9f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt @@ -0,0 +1,35 @@ +package com.tangem.tap.features.wallet.ui.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.extensions.dispatchDialogHide +import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.store +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class SimpleOkDialog { + + companion object { + fun create(dialog: AppDialog.SimpleOkDialog, context: Context): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(dialog.header) + setMessage(dialog.message) + setPositiveButton(R.string.common_ok) { _, _ -> } + setOnDismissListener { store.dispatchDialogHide() } + }.create() + } + + fun create(dialog: AppDialog.SimpleOkDialogRes, context: Context): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(context.getString(dialog.headerId)) + setMessage(dialog.messageId) + setPositiveButton(R.string.common_ok) { _, _ -> } + setOnDismissListener { store.dispatchDialogHide() } + }.create() + } + } + +} \ No newline at end of file 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" /> + + android:layout_weight="@dimen/home_bottom_weight" /> diff --git a/app/src/main/res/layout/fragment_onboarding_wallet.xml b/app/src/main/res/layout/fragment_onboarding_wallet.xml index 5df3b30845..2c1c940cb5 100644 --- a/app/src/main/res/layout/fragment_onboarding_wallet.xml +++ b/app/src/main/res/layout/fragment_onboarding_wallet.xml @@ -23,9 +23,9 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" + app:menu="@menu/shop" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" - app:title="@string/onboarding_getting_started" - app:menu="@menu/shop"/> + app:title="@string/onboarding_getting_started" /> @@ -33,6 +33,7 @@ android:id="@+id/onboarding_main_container" android:layout_width="match_parent" android:layout_height="match_parent" + android:clipChildren="false" app:layout_behavior="@string/appbar_scrolling_view_behavior"> @@ -51,28 +51,25 @@ android:id="@+id/imv_card_background" android:layout_width="236dp" android:layout_height="236dp" - android:layout_gravity="center" android:adjustViewBounds="true" android:background="@drawable/shape_circle" android:backgroundTint="@color/lightGray0" android:elevation="0dp" android:scaleType="fitCenter" - app:layout_constraintStart_toStartOf="@id/fl_cards_container" - app:layout_constraintEnd_toEndOf="@id/fl_cards_container" - app:layout_constraintTop_toTopOf="@id/fl_cards_container" app:layout_constraintBottom_toBottomOf="@id/fl_cards_container" - android:layout_marginBottom="16dp" - /> + app:layout_constraintEnd_toEndOf="@id/fl_cards_container" + app:layout_constraintStart_toStartOf="@id/fl_cards_container" + app:layout_constraintTop_toTopOf="@id/fl_cards_container" /> @@ -81,36 +78,33 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" - android:src="@drawable/card_placeholder_new" - android:visibility="gone" android:layout_marginStart="32dp" android:layout_marginEnd="32dp" android:scaleType="fitCenter" - - /> + android:src="@drawable/card_placeholder_black" + android:visibility="gone" /> + android:src="@drawable/card_placeholder_black" + android:visibility="gone" /> + android:src="@drawable/card_placeholder_black" + android:transitionName="imv_front_card" /> @@ -119,12 +113,12 @@ android:layout_width="142dp" android:layout_height="142dp" android:src="@drawable/ic_activation_success" + android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/guideline2" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.495" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/pb_state" - android:visibility="gone"/> + app:layout_constraintTop_toBottomOf="@id/pb_state" /> + app:layout_constraintTop_toTopOf="@+id/guideline2" + tools:text="Добавлена одна резервная карта" /> + app:layout_constraintTop_toBottomOf="@+id/tv_header" + tools:text="Body" /> + app:layout_constraintTop_toTopOf="@+id/guideline2" /> + app:tabPaddingStart="8dp" /> + app:layout_constraintBottom_toBottomOf="parent" /> diff --git a/app/src/main/res/layout/fragment_shop.xml b/app/src/main/res/layout/fragment_shop.xml new file mode 100644 index 0000000000..9ad04edbdd --- /dev/null +++ b/app/src/main/res/layout/fragment_shop.xml @@ -0,0 +1,375 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ - + - + - + diff --git a/app/src/main/res/layout/layout_backup_access_code_info.xml b/app/src/main/res/layout/layout_backup_access_code_info.xml index ac5a98171f..3ef7ba57fb 100644 --- a/app/src/main/res/layout/layout_backup_access_code_info.xml +++ b/app/src/main/res/layout/layout_backup_access_code_info.xml @@ -8,18 +8,18 @@ android:id="@+id/iv_access_code_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="25dp" + android:layout_marginTop="@dimen/access_code_info_protect_margin_top" android:src="@drawable/ic_access_code" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_balance_wallet_details.xml b/app/src/main/res/layout/layout_balance_wallet_details.xml index 56bf9b2d69..a7633bd26c 100644 --- a/app/src/main/res/layout/layout_balance_wallet_details.xml +++ b/app/src/main/res/layout/layout_balance_wallet_details.xml @@ -4,8 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingBottom="24dp"> + 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_onboarding_buttons_common.xml b/app/src/main/res/layout/layout_onboarding_buttons_common.xml index ff0472d121..e99f7dfe4a 100644 --- a/app/src/main/res/layout/layout_onboarding_buttons_common.xml +++ b/app/src/main/res/layout/layout_onboarding_buttons_common.xml @@ -1,33 +1,33 @@ + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + app:layout_constraintStart_toStartOf="parent" + tools:text="Main Action" /> + app:layout_constraintStart_toStartOf="parent" + tools:text="Alternative action" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_container_bottom.xml b/app/src/main/res/layout/layout_onboarding_container_bottom.xml index d5e9dbcc4a..08c351d6fb 100644 --- a/app/src/main/res/layout/layout_onboarding_container_bottom.xml +++ b/app/src/main/res/layout/layout_onboarding_container_bottom.xml @@ -10,14 +10,14 @@ android:id="@+id/linearLayoutCompat" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginBottom="8dp" + android:layout_marginBottom="@dimen/home_bottom_llCompat_margin_bottom" android:gravity="center" android:orientation="vertical" app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.1"> + app:layout_constraintVertical_bias="@dimen/home_bottom_llCompat_constraint_vertical_bias"> - + @@ -29,8 +35,10 @@ android:elevation="1dp" android:rotation="-3" android:scaleType="fitXY" + android:scaleX="@dimen/home_top_front_card_scaleX" + android:scaleY="@dimen/home_top_front_card_scaleY" android:src="@drawable/card_placeholder_black" - android:translationY="@dimen/home_front_card_translationY" + android:translationY="@dimen/home_top_front_card_translationY" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> 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/lp_onboarding_topup_wallet.xml b/app/src/main/res/layout/lp_onboarding_topup_wallet.xml index 6b924ea771..dc6efbcc96 100644 --- a/app/src/main/res/layout/lp_onboarding_topup_wallet.xml +++ b/app/src/main/res/layout/lp_onboarding_topup_wallet.xml @@ -25,7 +25,7 @@ + + + +