diff --git a/app/build.gradle b/app/build.gradle index 70f6e3df99..a670cb5c27 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,22 +1,22 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { applicationId "com.tangem.wallet" versionCode project.hasProperty('versionCode') ? project.property('versionCode') as int : 1 versionName project.hasProperty('versionName') ? project.property('versionName') : "1.0.0.-SNAPSHOT" minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 31 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - androidExtensions { - experimental = true + buildFeatures { + compose true + viewBinding true } buildTypes { release { @@ -47,12 +47,14 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } - compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 coreLibraryDesugaringEnabled false } + composeOptions { + kotlinCompilerExtensionVersion '1.1.0' + } packagingOptions { exclude 'lib/x86_64/darwin/libscrypt.dylib' exclude 'lib/x86_64/freebsd/libscrypt.so' @@ -67,19 +69,19 @@ repositories { dependencies { implementation fileTree(include: ['*.aar'], dir: 'libs') - implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'androidx.fragment:fragment-ktx:1.3.6' - implementation 'androidx.constraintlayout:constraintlayout:2.1.1' - implementation 'com.google.android.material:material:1.4.0' - implementation "androidx.core:core-ktx:1.6.0" - implementation 'com.google.android.play:core:1.10.2' + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.fragment:fragment-ktx:1.4.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation 'com.google.android.material:material:1.5.0' + implementation "androidx.core:core-ktx:1.7.0" + implementation 'com.google.android.play:core:1.10.3' implementation 'com.google.android.play:core-ktx:1.8.1' 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-139' + implementation 'com.tangem.tangem-sdk-kotlin:android:develop-139' // WebView implementation "androidx.browser:browser:1.3.0" @@ -129,6 +131,24 @@ 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' + + //Compose + implementation 'androidx.activity:activity-compose:1.4.0' + implementation 'androidx.compose.material:material:1.1.0' + implementation 'androidx.compose.animation:animation:1.1.0' + implementation 'androidx.compose.ui:ui-tooling:1.1.0' + implementation "com.google.accompanist:accompanist-appcompat-theme:0.23.0" + + implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6' + 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/debug/res/values-ru/strings.xml b/app/src/debug/res/values-ru/strings.xml new file mode 100644 index 0000000000..1abc31ee12 --- /dev/null +++ b/app/src/debug/res/values-ru/strings.xml @@ -0,0 +1,6 @@ + + + + DebugTangem + + \ No newline at end of file diff --git a/app/src/debug_beta/res/values-ru/strings.xml b/app/src/debug_beta/res/values-ru/strings.xml new file mode 100644 index 0000000000..3e2d147b31 --- /dev/null +++ b/app/src/debug_beta/res/values-ru/strings.xml @@ -0,0 +1,6 @@ + + + + BetaTangem + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f358215715..4dfca87b23 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,8 +39,12 @@ 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/IntentHandler.kt b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt index 7058973e1a..1e41053369 100644 --- a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt @@ -5,7 +5,6 @@ import android.nfc.NfcAdapter import android.nfc.Tag import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.topup.TradeCryptoHelper import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.home.redux.HomeAction @@ -15,6 +14,11 @@ import timber.log.Timber class IntentHandler { + private val TRANSACTION_ID_PARAM = "transactionId" + private val CURRENCY_CODE_PARAM = "baseCurrencyCode" + private val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount" + private val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress" + fun handleIntent(intent: Intent?) { handleBackgroundScan(intent) handleWalletConnectLink(intent) @@ -23,8 +27,8 @@ class IntentHandler { private fun handleBackgroundScan(intent: Intent?) { if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action || - NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action - ) + NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action + ) ) { val tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) if (tag != null) { @@ -45,13 +49,13 @@ class IntentHandler { private fun handleSellCurrencyCallback(intent: Intent?) { try { val transactionID = - intent?.data?.getQueryParameter(TradeCryptoHelper.TRANSACTION_ID_PARAM) ?: return + intent?.data?.getQueryParameter(TRANSACTION_ID_PARAM) ?: return val currency = - intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_CODE_PARAM) ?: return + intent.data?.getQueryParameter(CURRENCY_CODE_PARAM) ?: return val amount = - intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_AMOUNT_PARAM) ?: return + intent.data?.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return val destinationAddress = - intent.data?.getQueryParameter(TradeCryptoHelper.DEPOSIT_WALLET_ADDRESS_PARAM) + intent.data?.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") @@ -63,7 +67,7 @@ class IntentHandler { transactionId = transactionID )) } catch (exception: Exception) { - Timber.d("Not Moonpay URL") + Timber.d("Not MoonPay URL") } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt index caa5f0117b..424f06d9ce 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandler.kt @@ -1,5 +1,6 @@ package com.tangem.tap.common.analytics +import com.shopify.buy3.Storefront import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError import com.tangem.tap.common.extensions.filterNotNull @@ -13,6 +14,11 @@ abstract class AnalyticsHandler { params: Map = emptyMap() ) + abstract fun triggerEvent( + event: String, + params: Map + ) + abstract fun logCardSdkError( error: TangemSdkError, actionToLog: Analytics.ActionToLog, @@ -71,6 +77,13 @@ abstract class AnalyticsHandler { ) } } - } + + fun logShopifyOrder(order: Storefront.Order) { + triggerEvent(getOrderEvent(), getOrderParams(order)) + } + + protected abstract fun getOrderEvent(): String + + protected abstract fun getOrderParams(order: Storefront.Order): Map } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/AppsFlyerAnalyticsHandler.kt index 9692defe86..64beb11b4a 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AppsFlyerAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AppsFlyerAnalyticsHandler.kt @@ -1,7 +1,10 @@ package com.tangem.tap.common.analytics import android.app.Application +import com.appsflyer.AFInAppEventParameterName +import com.appsflyer.AFInAppEventType import com.appsflyer.AppsFlyerLib +import com.shopify.buy3.Storefront import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError @@ -30,4 +33,30 @@ class AppsFlyerAnalyticsHandler(val context: Application): AnalyticsHandler() { //TODO } + override fun getOrderEvent(): String = AFInAppEventType.PURCHASE + + override fun getOrderParams(order: Storefront.Order): Map { + + val sku = order.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown" + + val discountCode = + (order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code + + val discountParams = if (discountCode != null ){ + mapOf(AFInAppEventParameterName.COUPON_CODE to discountCode) + } else { + mapOf() + } + + return mapOf( + AFInAppEventParameterName.CONTENT_ID to sku, + AFInAppEventParameterName.REVENUE to order.totalPriceV2.amount, + AFInAppEventParameterName.CURRENCY to order.currencyCode.name + ) + discountParams + } + + override fun triggerEvent(event: String, params: Map) { + AppsFlyerLib.getInstance().logEvent(context, event, params) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt index 106c07aae9..4e0371206a 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/FirebaseAnalyticsHandler.kt @@ -2,9 +2,11 @@ package com.tangem.tap.common.analytics import android.os.Bundle import androidx.core.os.bundleOf +import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.ktx.analytics import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.ktx.Firebase +import com.shopify.buy3.Storefront import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError @@ -57,4 +59,30 @@ object FirebaseAnalyticsHandler : AnalyticsHandler() { params.forEach { (key, value) -> FirebaseCrashlytics.getInstance().setCustomKey(key, value) } FirebaseCrashlytics.getInstance().recordException(error) } + + override fun getOrderEvent(): String = FirebaseAnalytics.Event.PURCHASE + + override fun getOrderParams(order: Storefront.Order): Map { + + val sku = order.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown" + + val discountCode = + (order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code + + val discountParams = if (discountCode != null ){ + mapOf(FirebaseAnalytics.Param.DISCOUNT to discountCode) + } else { + mapOf() + } + + return mapOf( + FirebaseAnalytics.Param.ITEM_ID to sku, + FirebaseAnalytics.Param.VALUE to order.totalPriceV2.amount, + FirebaseAnalytics.Param.CURRENCY to order.currencyCode.name + ) + discountParams + } + + override fun triggerEvent(event: String, params: Map) { + Firebase.analytics.logEvent(event, params.toBundle()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt index da77c37e05..b4824b60a3 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.analytics import android.app.Application +import com.shopify.buy3.Storefront import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError @@ -15,6 +16,10 @@ class GlobalAnalyticsHandler(val analyticsHandlers: List) : analyticsHandlers.forEach { it.triggerEvent(event, card, blockchain) } } + override fun triggerEvent(event: String, params: Map) { + analyticsHandlers.forEach { it.triggerEvent(event, params) } + } + override fun logCardSdkError( error: TangemSdkError, actionToLog: Analytics.ActionToLog, @@ -28,6 +33,14 @@ class GlobalAnalyticsHandler(val analyticsHandlers: List) : analyticsHandlers.forEach { it.logError(error, params) } } + override fun getOrderEvent(): String { + return "" + } + + override fun getOrderParams(order: Storefront.Order): Map { + return emptyMap() + } + companion object { fun createDefaultAnalyticHandlers(context: Application): GlobalAnalyticsHandler { return GlobalAnalyticsHandler( 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/Picasso.kt b/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt index f34b5e9ecf..81e215bcf9 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt @@ -1,12 +1,12 @@ package com.tangem.tap.common.extensions -import android.graphics.PorterDuff +import android.graphics.* import android.net.Uri -import android.widget.ImageView import android.widget.TextView -import androidx.core.content.ContextCompat +import androidx.constraintlayout.utils.widget.ImageFilterView import com.squareup.picasso.Callback import com.squareup.picasso.Picasso +import com.squareup.picasso.Transformation import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil import com.tangem.blockchain.common.Token @@ -14,7 +14,7 @@ import com.tangem.tap.domain.extensions.getCustomIconUrl import com.tangem.wallet.R fun Picasso.loadCurrenciesIcon( - imageView: ImageView, + imageView: ImageFilterView, textView: TextView, token: Token? = null, blockchain: Blockchain, @@ -42,6 +42,7 @@ fun Picasso.loadCurrenciesIcon( setTokenImage(imageView, textView, token) } this.load(url) + .transform(RoundedCornersTransform()) .noPlaceholder() ?.into(imageView, object : Callback { @@ -54,7 +55,7 @@ fun Picasso.loadCurrenciesIcon( imageView.colorFilter = null textView.text = null } - if (blockchain.isTestnet()) imageView.tint(R.color.tint) + if (blockchain.isTestnet()) imageView.saturation = 0f } }) } @@ -68,42 +69,64 @@ private const val QCX = "QCX" private const val VOYR = "VOYRME" private fun setOfflineCurrencyImage( - imageView: ImageView, + imageView: ImageFilterView, textView: TextView, token: Token?, blockchain: Blockchain, ) { - if (token != null) { - setTokenImage(imageView, textView, token) - } else { - setBlockchainImage(imageView, textView, blockchain) + when (token) { + null -> setBlockchainImage(imageView, textView, blockchain) + else -> setTokenImage(imageView, textView, token) } - if (blockchain.isTestnet()) imageView.tint(R.color.tint) -} - -fun ImageView.tint(colorRes: Int) { -// val color = ContextCompat.getColor(context, colorRes); -// ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(color)); - this.setColorFilter(ContextCompat.getColor(context, colorRes), PorterDuff.Mode.DARKEN); - } private fun setBlockchainImage( - imageView: ImageView, + imageView: ImageFilterView, textView: TextView, blockchain: Blockchain, ) { imageView.setImageResource(blockchain.getIconRes()) imageView.colorFilter = null + if (blockchain.isTestnet()) imageView.saturation = 0f textView.text = null } private fun setTokenImage( - imageView: ImageView, + imageView: ImageFilterView, textView: TextView, token: Token, ) { imageView.setImageResource(R.drawable.shape_circle) - imageView.setColorFilter(token.getColor()) + if (token.blockchain.isTestnet()) { + imageView.saturation = 0f + } else { + imageView.setColorFilter(token.getColor()) + } textView.text = token.symbol.take(1) } + +private class RoundedCornersTransform : Transformation { + + override fun transform(source: Bitmap): Bitmap { + val size = source.width.coerceAtMost(source.height) + val x = (source.width - size) / 2 + val y = (source.height - size) / 2 + val squaredBitmap = Bitmap.createBitmap(source, x, y, size, size) + if (squaredBitmap != source) source.recycle() + + val paint = Paint().apply { + shader = BitmapShader(squaredBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + isAntiAlias = true + } + + val rectF = RectF(0f, 0f, source.width.toFloat(), source.height.toFloat()) + val radius = size / 8f + val bitmap = Bitmap.createBitmap(size, size, source.config) + Canvas(bitmap).drawRoundRect(rectF, radius, radius, paint) + squaredBitmap.recycle() + + return bitmap + } + + override fun key(): String = "rounded_corners" +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/String.kt b/app/src/main/java/com/tangem/tap/common/extensions/String.kt index f0f4ef1d00..7d54a7bf92 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/String.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/String.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.extensions import android.content.Context import android.graphics.Bitmap import android.graphics.Color +import android.net.Uri import android.text.Spannable import android.text.style.ForegroundColorSpan import androidx.core.content.ContextCompat @@ -21,24 +22,24 @@ fun String?.ellipsizeBeforeSpace(allowedSize: Int): String { val startIndex = endIndex - sizeDifference val newString = this.removeRange(startIndex, endIndex) return newString.substring(0 until startIndex) + "..." + - newString.substring(startIndex until newString.length) + newString.substring(startIndex until newString.length) } fun String.colorSegment( - context: Context, - color: Int, - startIndex: Int = 0, - endIndex: Int = this.length + context: Context, + color: Int, + startIndex: Int = 0, + endIndex: Int = this.length ): Spannable { return this.toSpannable() - .also { spannable -> - spannable.setSpan( - ForegroundColorSpan(ContextCompat.getColor(context, color)), - startIndex, - endIndex, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } + .also { spannable -> + spannable.setSpan( + ForegroundColorSpan(ContextCompat.getColor(context, color)), + startIndex, + endIndex, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } } fun String.toQrCode(): Bitmap { @@ -59,4 +60,6 @@ fun String.toQrCode(): Bitmap { } } return bmp -} \ No newline at end of file +} + +fun String.urlEncode(): String = Uri.encode(this) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/StringBuilder.kt b/app/src/main/java/com/tangem/tap/common/extensions/StringBuilder.kt index 132b8cc44c..3f8fa992ff 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/StringBuilder.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/StringBuilder.kt @@ -3,6 +3,20 @@ package com.tangem.tap.common.extensions /** [REDACTED_AUTHOR] */ -fun StringBuilder.appendIf(value: String, predicate: (String) -> Boolean) { +fun StringBuilder.appendIf(value: String, predicate: (String) -> Boolean): StringBuilder { if (predicate(value)) this.append(value) + return this +} + +fun StringBuilder.appendIfNotNull(value: String?, prefix: String? = null, postfix: String? = null): StringBuilder { + if (value == null) return this + + prefix?.let { append(it) } + append(value) + postfix?.let { append(it) } + return this +} + +fun String.appendIfNotNull(value: String?, prefix: String? = null, postfix: String? = null): String { + return StringBuilder(this).apply { appendIfNotNull(value, prefix, postfix) }.toString() } \ No newline at end of file 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..d241f8768c 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 @@ -37,10 +37,19 @@ fun Context.getColorCompat(@ColorRes colorRes: Int): Int { return ContextCompat.getColor(this, colorRes) } +@ColorInt +fun View.getColor(@ColorRes colorRes: Int): Int { + return ContextCompat.getColor(context, colorRes) +} + fun View.getString(@StringRes id: Int): String { return context.getString(id) } +fun View.getString(@StringRes id: Int, vararg formatArgs: String): String { + return context.getString(id, *formatArgs) +} + fun View.getResourceName(): String { return try { resources.getResourceEntryName(id) @@ -68,9 +77,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 = @@ -158,4 +174,8 @@ fun Context.safeStartActivity( } finally { finally?.invoke() } +} + +fun View.getString(resId: Int, vararg formatArgs: Any?): String { + return context.getString(resId, formatArgs) } \ No newline at end of file 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..b76e63b0e2 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 @@ -7,19 +7,29 @@ import com.tangem.common.services.Result 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.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.store +import kotlinx.coroutines.delay +import timber.log.Timber /** [REDACTED_AUTHOR] */ suspend fun WalletManager.safeUpdate(): Result = try { - update() - Result.Success(wallet) + if (isDemoWallet()) { + delay(500) + Result.Success(wallet) + } else { + update() + Result.Success(wallet) + } } catch (exception: Exception) { + Timber.e(exception) + if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { Result.Failure(TapError.NoInternetConnection) } else { @@ -36,17 +46,17 @@ suspend fun WalletManager.safeUpdate(): Result = try { } fun WalletManager?.getToUpUrl(): String? { + val globalState = store.state.globalState + val currencyExchangeManager = globalState.currencyExchangeManager ?: return null val wallet = this?.wallet ?: return null - val config = store.state.globalState.configManager?.config ?: return null - val defaultAddress = wallet.address - return TradeCryptoHelper.getUrl( - TradeCryptoHelper.Action.Buy, - wallet.blockchain, - wallet.blockchain.currency, - defaultAddress, - config.moonPayApiKey, - config.moonPayApiSecretKey + val defaultAddress = wallet.address + return currencyExchangeManager.getUrl( + action = CurrencyExchangeManager.Action.Buy, + blockchain = wallet.blockchain, + cryptoCurrencyName = wallet.blockchain.currency, + fiatCurrency = globalState.appCurrency, + walletAddress = defaultAddress, ) } diff --git a/app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt b/app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt index 36e92a0ebe..1999c35855 100644 --- a/app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt +++ b/app/src/main/java/com/tangem/tap/common/leapfrogWidget/TestLeapfrogFragment.kt @@ -5,15 +5,18 @@ import android.view.View import android.widget.FrameLayout import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater +import by.kirich1409.viewbindingdelegate.viewBinding import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidgetState import com.tangem.tap.domain.twins.TwinsCardWidget import com.tangem.wallet.R -import kotlinx.android.synthetic.main.test_leapfrog_fragment.* +import com.tangem.wallet.databinding.TestLeapfrogFragmentBinding class TestLeapfrogFragment : Fragment(R.layout.test_leapfrog_fragment) { private lateinit var twinsCardWidget: TwinsCardWidget + private val binding: TestLeapfrogFragmentBinding by viewBinding(TestLeapfrogFragmentBinding::bind) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -28,30 +31,29 @@ class TestLeapfrogFragment : Fragment(R.layout.test_leapfrog_fragment) { val leapfrog = LeapfrogWidget(leapfrogContainer) twinsCardWidget = TwinsCardWidget(leapfrog) { 200f } - btn_twin_welcome.setOnClickListener { + binding.btnTwinWelcome.setOnClickListener { twinsCardWidget.toWelcome() } - btn_twin_to_leapfrog.setOnClickListener { + binding.btnTwinToLeapfrog.setOnClickListener { twinsCardWidget.toLeapfrog() } - btn_twin_activate.setOnClickListener { + binding.btnTwinActivate.setOnClickListener { twinsCardWidget.toActivate() } - - btn_lp_init.setOnClickListener { + binding.btnLpInit.setOnClickListener { leapfrog.initViews() } - btn_lp_unfold.setOnClickListener { + binding.btnLpUnfold.setOnClickListener { leapfrog.unfold() } - btn_lp_fold.setOnClickListener { + binding.btnLpFold.setOnClickListener { leapfrog.fold() } - btn_lp_leap.setOnClickListener { + binding.btnLpLeap.setOnClickListener { leapfrog.leap() } - btn_lp_leap_back.setOnClickListener { + binding.btnLpLeapBack.setOnClickListener { leapfrog.leapBack() } } 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..6419e09ac2 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 @@ -1,5 +1,6 @@ package com.tangem.tap.common.redux +import com.tangem.common.extensions.VoidCallback import com.tangem.tap.features.wallet.redux.AddressData import com.tangem.tap.features.wallet.redux.Currency @@ -9,6 +10,8 @@ import com.tangem.tap.features.wallet.redux.Currency interface StateDialog sealed class AppDialog : StateDialog { + data class SimpleOkDialog(val header: String, val message: String, val onOk: VoidCallback? = null) : AppDialog() + data class SimpleOkDialogRes(val headerId: Int, val messageId: Int, val onOk: VoidCallback? = null) : AppDialog() object ScanFailsDialog : AppDialog() data class AddressInfoDialog( val currency: Currency, diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 04b8f27743..58684ac334 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -14,7 +14,7 @@ import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.feedback.EmailData import com.tangem.tap.features.feedback.FeedbackManager -import com.tangem.tap.network.moonpay.MoonpayStatus +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import org.rekotlin.Action sealed class GlobalAction : Action { @@ -76,7 +76,7 @@ sealed class GlobalAction : Action { data class SendFeedback(val emailData: EmailData) : GlobalAction() data class UpdateFeedbackInfo(val walletManagers: List) : GlobalAction() - object GetMoonPayStatus : GlobalAction() { - data class Success(val moonPayStatus: MoonpayStatus) : GlobalAction() + object InitCurrencyExchangeManager : GlobalAction() { + data class Success(val exchangeManager: CurrencyExchangeManager) : GlobalAction() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 97fc6ac162..7675e42b03 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -2,7 +2,7 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError -import com.tangem.common.services.Result +import com.tangem.common.extensions.ifNotNull import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain @@ -12,10 +12,11 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.network.moonpay.MoonpayService +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager +import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService +import com.tangem.tap.network.exchangeServices.onramper.OnramperService import kotlinx.coroutines.launch import org.rekotlin.Middleware -import timber.log.Timber class GlobalMiddleware { companion object { @@ -69,19 +70,21 @@ private val globalMiddlewareHandler: Middleware = { dispatch, appState } is GlobalAction.UpdateFeedbackInfo -> { store.state.globalState.feedbackManager?.infoHolder - ?.setWalletsInfo(action.walletManagers) + ?.setWalletsInfo(action.walletManagers) } - is GlobalAction.GetMoonPayStatus -> { - val apiKey = appState()?.globalState?.configManager?.config?.moonPayApiKey - if (apiKey != null) { + is GlobalAction.InitCurrencyExchangeManager -> { + val config = appState()?.globalState?.configManager?.config + ifNotNull( + config?.onramperApiKey, + config?.moonPayApiKey, + config?.moonPayApiSecretKey, + ) { onramperKey, moonPayKey, moonPaySecretKey -> scope.launch { - val result = MoonpayService().getMoonpayStatus(apiKey) - when (result) { - is Result.Success -> { - store.dispatchOnMain(GlobalAction.GetMoonPayStatus.Success(result.data)) - } - is Result.Failure -> Timber.e(result.error) - } + val onramper = OnramperService(onramperKey) + val moonPay = MoonPayService(moonPayKey, moonPaySecretKey) + val exchangeManager = CurrencyExchangeManager(onramper, moonPay) + exchangeManager.getStatus() + store.dispatchOnMain(GlobalAction.InitCurrencyExchangeManager.Success(exchangeManager)) } } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 94621117bc..d84c43379d 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -2,7 +2,6 @@ package com.tangem.tap.common.redux.global import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.onboarding.OnboardingManager -import com.tangem.tap.network.moonpay.MoonpayStatus import com.tangem.tap.preferencesStorage import org.rekotlin.Action @@ -19,7 +18,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState { is GlobalAction.Onboarding.Start -> { val onboardingManager = if (action.scanResponse != null) { val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage - OnboardingManager(action.scanResponse, usedCardsPrefStorage) + OnboardingManager(action.scanResponse, usedCardsPrefStorage) } else { null } @@ -67,19 +66,8 @@ fun globalReducer(action: Action, state: AppState): GlobalState { is GlobalAction.HideDialog -> { globalState.copy(dialog = null) } - is GlobalAction.GetMoonPayStatus.Success -> { - val fiatExchangeIsEnabled = globalState.configManager?.config?.isTopUpEnabled ?: false - val moonpayStatus = if (fiatExchangeIsEnabled) { - action.moonPayStatus - } else { - MoonpayStatus( - isBuyAllowed = false, - isSellAllowed = false, - availableToBuy = emptyList(), - availableToSell = emptyList() - ) - } - globalState.copy(moonpayStatus = moonpayStatus) + is GlobalAction.InitCurrencyExchangeManager.Success -> { + globalState.copy(currencyExchangeManager = action.exchangeManager) } is GlobalAction.SetIfCardVerifiedOnline -> globalState.copy(cardVerifiedOnline = action.verified) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index c607125cb8..ace0a06ae7 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -11,7 +11,7 @@ import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.feedback.FeedbackManager import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.network.coinmarketcap.CoinMarketCapService -import com.tangem.tap.network.moonpay.MoonpayStatus +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import org.rekotlin.StateType data class GlobalState( @@ -27,7 +27,7 @@ data class GlobalState( val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY, val scanCardFailsCounter: Int = 0, val dialog: StateDialog? = null, - val moonpayStatus: MoonpayStatus? = null, + val currencyExchangeManager: CurrencyExchangeManager? = null, val resources: AndroidResources = AndroidResources(), val analyticsHandlers: AnalyticsHandler? = null, ) : StateType 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..3da94dc568 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt @@ -0,0 +1,230 @@ +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.analytics.AnalyticsHandler +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.ShopifyService +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 + } + + suspend fun waitForCheckout( + productType: ProductType, + analyticsHandler: AnalyticsHandler? + ) { + val result = shopifyService.checkout(true, checkouts[productType]!!.id) + result.onSuccess { + if (it.order != null) { + analyticsHandler?.logShopifyOrder(order = it.order) + } + } + } + + 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..a33fb82b84 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt @@ -0,0 +1,383 @@ +package com.tangem.tap.common.shop.shopify + +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.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?.order == 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/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 5712a5df13..bb104a4c80 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -153,7 +153,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse) { tangemSdk.config.cardIdDisplayFormat = if (scanResponse.isTangemTwins()) { - CardIdDisplayFormat.LastLunh(4) + CardIdDisplayFormat.LastLuhn(4) } else { CardIdDisplayFormat.Full } 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..55fc7f2154 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -1,24 +1,33 @@ package com.tangem.tap.domain +import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.* import com.tangem.common.services.Result import com.tangem.tap.common.extensions.dispatchDebugErrorNotification +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.safeUpdate +import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.common.redux.global.GlobalAction 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.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.network.NetworkConnectivity import com.tangem.tap.network.coinmarketcap.CoinMarketCapService +import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.math.BigDecimal @@ -30,7 +39,7 @@ class TapWalletManager { store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() } val walletManagerFactory: WalletManagerFactory - by lazy { WalletManagerFactory(blockchainSdkConfig) } + by lazy { WalletManagerFactory(blockchainSdkConfig) } suspend fun loadWalletData(walletManager: WalletManager) { handleUpdateWalletResult(walletManager.safeUpdate(), walletManager) @@ -51,8 +60,8 @@ class TapWalletManager { suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) { val currencies = wallet.getTokens() - .map { Currency.Token(it) } - .plus(Currency.Blockchain(wallet.blockchain)) + .map { Currency.Token(it) } + .plus(Currency.Blockchain(wallet.blockchain)) loadFiatRate(fiatCurrency, currencies) } @@ -89,7 +98,7 @@ class TapWalletManager { configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled) configManager?.turnOff(ConfigManager.isTopUpEnabled) } else if (blockchain == Blockchain.Bitcoin - || data.walletData?.blockchain == Blockchain.Bitcoin.id) { + || data.walletData?.blockchain == Blockchain.Bitcoin.id) { configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) configManager?.resetToDefault(ConfigManager.isTopUpEnabled) } else { @@ -129,8 +138,7 @@ class TapWalletManager { loadMultiWalletData(data, blockchain, null) } } - val moonPayStatus = store.state.globalState.moonpayStatus - store.dispatch(WalletAction.LoadWallet(moonPayStatus)) + store.dispatch(WalletAction.LoadWallet()) store.dispatch(WalletAction.LoadFiatRate()) } } @@ -156,7 +164,7 @@ class TapWalletManager { CardCurrencies(blockchains = blockchains, tokens = emptyList()) )) val walletManagers = - walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains.toList()) + walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains.toList()) store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers)) store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains.toList())) } @@ -165,12 +173,12 @@ class TapWalletManager { } else { val blockchains = savedCurrencies.blockchains.toList() val walletManagers = if ( - primaryTokens.isNotEmpty() && - primaryWalletManager != null && primaryBlockchain != null + primaryTokens.isNotEmpty() && + primaryWalletManager != null && primaryBlockchain != null ) { val blockchainsWithoutPrimary = blockchains.filterNot { it == primaryBlockchain } walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchainsWithoutPrimary) - .plus(primaryWalletManager) + .plus(primaryWalletManager) } else { walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains) } @@ -192,8 +200,7 @@ class TapWalletManager { return@withContext } - val moonPayStatus = store.state.globalState.moonpayStatus - store.dispatch(WalletAction.LoadWallet(moonPayStatus)) + store.dispatch(WalletAction.LoadWallet()) store.dispatch(WalletAction.LoadFiatRate()) } } @@ -207,6 +214,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 } @@ -217,7 +227,10 @@ class TapWalletManager { private suspend fun handleUpdateWalletResult(result: Result, walletManager: WalletManager) { withContext(Dispatchers.Main) { when (result) { - is Result.Success -> store.dispatch(WalletAction.LoadWallet.Success(result.data)) + is Result.Success -> { + checkForRentWarning(walletManager) + store.dispatch(WalletAction.LoadWallet.Success(result.data)) + } is Result.Failure -> { val error = (result.error as? TapError) ?: TapError.UnknownError when (error) { @@ -226,11 +239,11 @@ class TapWalletManager { } is TapError.WalletManagerUpdate.NoAccountError -> { store.dispatch(WalletAction.LoadWallet - .NoAccount(walletManager.wallet, error.customMessage)) + .NoAccount(walletManager.wallet, error.customMessage)) } is TapError.WalletManagerUpdate.InternalError -> { store.dispatch(WalletAction.LoadWallet - .Failure(walletManager.wallet, error.customMessage)) + .Failure(walletManager.wallet, error.customMessage)) } else -> { store.dispatchDebugErrorNotification(error) @@ -241,6 +254,38 @@ class TapWalletManager { } } + private fun checkForRentWarning(walletManager: WalletManager) { + val rentProvider = walletManager as? RentProvider ?: return + + scope.launch { + when (val result = rentProvider.minimalBalanceForRentExemption()) { + is com.tangem.blockchain.extensions.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 + store.dispatchOnMain(WalletAction.SetWalletRent( + blockchain = walletManager.wallet.blockchain, + minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"), + rentExempt = ("${rentExempt.stripZeroPlainString()} $currency") + )) + } + is com.tangem.blockchain.extensions.Result.Failure -> {} + } + } + } + private suspend fun handleFiatRatesResult(results: List?>>) { withContext(Dispatchers.Main) { results.map { diff --git a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt index ffc8df824c..c0f9c1e239 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt @@ -63,6 +63,8 @@ object TapWorkarounds { "AB04" to Blockchain.Dogecoin, "AB05" to Blockchain.BSC, "AB06" to Blockchain.XRP, + "AB07" to Blockchain.Bitcoin, + "AB08" to Blockchain.Ethereum, ) } 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..cb587f277b 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 /** @@ -10,13 +11,15 @@ import com.tangem.tap.domain.configurable.Loader data class Config( val coinMarketCapKey: String = "f6622117-c043-47a0-8975-9d673ce484de", val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE", + val onramperApiKey: String = "pk_test_Ix2aCF3ej_5tcDKkBR7MChIvf5Nb0oPORPQ3Oal5G8I0", val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C", val appsFlyerDevKey: String = "", val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(), 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 +55,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) } } @@ -79,6 +82,7 @@ class ConfigManager( config = config.copy( coinMarketCapKey = values.coinMarketCapKey, moonPayApiKey = values.moonPayApiKey, + onramperApiKey = values.onramperApiKey, moonPayApiSecretKey = values.moonPayApiSecretKey, blockchainSdkConfig = BlockchainSdkConfig( blockchairApiKey = values.blockchairApiKey, @@ -87,10 +91,12 @@ class ConfigManager( infuraProjectId = values.infuraProjectId ), appsFlyerDevKey = values.appsFlyerDevKey, + shopify = values.shopifyShop, ) defaultConfig = defaultConfig.copy( coinMarketCapKey = values.coinMarketCapKey, moonPayApiKey = values.moonPayApiKey, + onramperApiKey = values.onramperApiKey, moonPayApiSecretKey = values.moonPayApiSecretKey, blockchainSdkConfig = BlockchainSdkConfig( blockchairApiKey = values.blockchairApiKey, @@ -99,6 +105,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..41d1ea2022 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] */ @@ -14,12 +16,14 @@ class FeatureModel( class ConfigValueModel( val coinMarketCapKey: String, val moonPayApiKey: String, + val onramperApiKey: String, val moonPayApiSecretKey: String, val blockchairApiKey: String?, val blockchairAuthorizationToken: String?, 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..163ac1264a 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( @@ -162,13 +162,25 @@ class WarningMessagesManager( "", type = WarningMessage.Type.TestCard, priority = WarningMessage.Priority.Critical, - listOf(WarningMessage.Location.MainScreen), + listOf(WarningMessage.Location.MainScreen, WarningMessage.Location.SendScreen), null, R.string.alert_title, R.string.warning_testnet_card_message, 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/extensions/MoonpayStatus.kt b/app/src/main/java/com/tangem/tap/domain/extensions/CurrencyExchangeManager.kt similarity index 55% rename from app/src/main/java/com/tangem/tap/domain/extensions/MoonpayStatus.kt rename to app/src/main/java/com/tangem/tap/domain/extensions/CurrencyExchangeManager.kt index bd4b7af891..1d39e53861 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/MoonpayStatus.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/CurrencyExchangeManager.kt @@ -2,13 +2,22 @@ package com.tangem.tap.domain.extensions import com.tangem.blockchain.common.Blockchain import com.tangem.tap.features.wallet.redux.Currency -import com.tangem.tap.network.moonpay.MoonpayStatus +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager +import com.tangem.tap.network.exchangeServices.CurrencyExchangeStatus import com.tangem.tap.store /** [REDACTED_AUTHOR] */ -fun MoonpayStatus.buyIsAllowed(currency: Currency): Boolean { +fun CurrencyExchangeManager.buyIsAllowed(currency: Currency): Boolean { + return this.status?.buyIsAllowed(currency) ?: false +} + +fun CurrencyExchangeManager.sellIsAllowed(currency: Currency): Boolean { + return this.status?.sellIsAllowed(currency) ?: false +} + +fun CurrencyExchangeStatus.buyIsAllowed(currency: Currency): Boolean { if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false if (!isBuyAllowed) return false @@ -25,16 +34,17 @@ fun MoonpayStatus.buyIsAllowed(currency: Currency): Boolean { } } -fun MoonpayStatus.sellIsAllowed(currency: Currency): Boolean { +fun CurrencyExchangeStatus.sellIsAllowed(currency: Currency): Boolean { if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false if (!isSellAllowed) return false return when (currency) { is Currency.Blockchain -> { - if (currency.blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC) { - false - } else { - availableToSell.contains(currency.currencySymbol) + val blockchain = currency.blockchain + when { + blockchain.isTestnet() -> false + blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false + else -> availableToSell.contains(currency.currencySymbol) } } is Currency.Token -> false 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..1d46b341ec 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,20 @@ private class CreateWalletTangemWallet : ProductCommandProcessor { primaryCard = result.data - deriveKeys(session, callback) + when { + card.settings.isHDWalletAllowed -> { + deriveKeys(createWalletResponse, session, callback) + } + else -> { + callback( + CompletionResult.Success( + CreateProductWalletTaskResponse( + card = session.environment.card!!, primaryCard = primaryCard + ) + ) + ) + } + } } is CompletionResult.Failure -> { callback(CompletionResult.Failure(result.error)) @@ -149,28 +168,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 +205,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/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 271b9bf761..31931af76f 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -1,6 +1,5 @@ package com.tangem.tap.domain.tasks.product -import com.tangem.* import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.common.CompletionResult @@ -241,7 +240,7 @@ private class ScanWalletProcessor( callback: (result: CompletionResult) -> Unit ) { val derivations = collectDerivations(card) - if (derivations.isEmpty()) { + if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { callback( CompletionResult.Success( ScanResponse( 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..bb1d5c74bc 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,12 @@ 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 +import timber.log.Timber 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 +31,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) { @@ -117,16 +123,21 @@ class CurrenciesRepository(val context: Application) { getSupportedTokens() } - val blockchainTokens = supportedTokens.map { - fromJsonToTokensDao(loadTokensJson(it), it) + val blockchainTokens = supportedTokens.mapNotNull { blockchain -> + loadTokensJson(blockchain)?.let { fromJsonToTokensDao(it, blockchain) } } val tokens = blockchainTokens.flatten().map { it.toToken() } return tokens } - private fun loadTokensJson(blockchain: Blockchain): String { + private fun loadTokensJson(blockchain: Blockchain): String? { val fileName = getFileName(blockchain) - return context.assets.readJsonFileToString(fileName) + return try { + context.assets.readJsonFileToString(fileName) + } catch (ex: Exception) { + Timber.e(ex, "Tokens with the file name %s not found", fileName) + null + } } private fun getFileName(blockchain: Blockchain): String { @@ -143,12 +154,12 @@ class CurrenciesRepository(val context: Application) { private fun getSupportedTokens(): List { return listOf( - Blockchain.Avalanche, - Blockchain.Binance, - Blockchain.BSC, Blockchain.Ethereum, + Blockchain.BSC, + Blockchain.Binance, Blockchain.Polygon, - Blockchain.Solana, + Blockchain.Avalanche, + Blockchain.Fantom, ) } @@ -164,11 +175,11 @@ class CurrenciesRepository(val context: Application) { return excludeUnsupportedBlockchains(blockchains) } - //TODO: move to the App settings + // Use this list to temporarily exclude a blockchain from the list of tokens. private fun excludeUnsupportedBlockchains(blockchains: List): List { return blockchains.toMutableList().apply { removeAll(listOf( - Blockchain.Fantom, Blockchain.FantomTestnet +// Any blockchain )) } } @@ -183,6 +194,13 @@ class CurrenciesRepository(val context: Application) { } } +fun Blockchain.getTokensName(): String { + return when (this) { + Blockchain.Fantom -> "Fantom Opera" + else -> this.fullName + } +} + @JsonClass(generateAdapter = true) data class TokenDao( val name: String, 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 deleted file mode 100644 index 9dd6e91960..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/topup/TopUpManager.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.tap.domain.topup - -import com.tangem.Message -import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager -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.redux.global.GlobalAction -import com.tangem.tap.domain.TangemSigner -import com.tangem.tap.store -import com.tangem.tap.tangemSdk -import java.math.BigDecimal - -class TopUpManager { - suspend fun topUpTestErc20Tokens(walletManager: EthereumWalletManager, token: Token) { - walletManager.update() - - val amountToSend = Amount(walletManager.wallet.blockchain) - val destinationAddress = token.contractAddress - - val feeResult = walletManager.getFee(amountToSend, - destinationAddress) as? Result.Success ?: return - val fee = feeResult.data[0] - - if ((walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO) < fee.value) { - return - } - - val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress) - - val signer = TangemSigner( - tangemSdk = tangemSdk, Message() - ) { signResponse -> - store.dispatch( - GlobalAction.UpdateWalletSignedHashes( - walletSignedHashes = signResponse.totalSignedHashes, - walletPublicKey = walletManager.wallet.publicKey.seedKey, - remainingSignatures = signResponse.remainingSignatures - ) - ) - } - walletManager.send(transaction, signer) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/topup/TradeCryptoHelper.kt b/app/src/main/java/com/tangem/tap/domain/topup/TradeCryptoHelper.kt deleted file mode 100644 index ec92f9f0fe..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/topup/TradeCryptoHelper.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.tap.domain.topup - -import android.net.Uri -import android.util.Base64 -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.common.redux.global.CryptoCurrencyName -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec - -class TradeCryptoHelper { - - companion object { - - const val TRANSACTION_ID_PARAM = "transactionId" - const val CURRENCY_CODE_PARAM = "baseCurrencyCode" - const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount" - const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress" - - private const val REDIRECT_URL_BUY = "tangem://success.tangem.com" - private const val REDIRECT_URL_SELL_REQUEST = "tangem://sell-request.tangem.com" - - private const val BASE_URL_BUY = - "https://buy.moonpay.io" -// "https://buy-staging.moonpay.io" // TESTNET - private const val BASE_URL_SELL = - "https://sell.moonpay.com" -// "https://sell-staging.moonpay.com" // TESTNET - - private const val API_KEY_PATH = "?apiKey=" - private const val CURRENCY_PATH = "¤cyCode=" - private const val WALLET_ADDRESS_PATH = "&walletAddress=" - - private const val REFUND_WALLET_ADDRESS_PATH = "&refundWalletAddress=" - private const val BASE_CURRENCY_PATH = "&baseCurrencyCode=" - - private const val REDIRECT_URL_PATH = "&redirectURL=" - private const val SIGNATURE_PATH = "&signature=" - - private const val TRANSACTION_RECEIPT_PATH = "transaction_receipt?transactionId=" - - private const val BASE_CURRENCY_USD = "USD" - - fun getUrl( - action: Action, - blockchain: Blockchain?, - cryptoCurrencyName: CryptoCurrencyName, - walletAddress: String, - apiKey: String, - secretKey: String, - ): String { - val originalQuery: String - val baseUrl: String - when (action) { - Action.Buy -> { - if (blockchain?.isTestnet() == true) { - return blockchain.getTestnetTopUpUrl() ?: "" - } - baseUrl = BASE_URL_BUY - originalQuery = API_KEY_PATH + apiKey.urlEncode() + - CURRENCY_PATH + cryptoCurrencyName.urlEncode() + - BASE_CURRENCY_PATH + BASE_CURRENCY_USD.urlEncode() + - WALLET_ADDRESS_PATH + walletAddress.urlEncode() + - REDIRECT_URL_PATH + REDIRECT_URL_BUY.urlEncode() - } - Action.Sell -> { - baseUrl = BASE_URL_SELL - originalQuery = API_KEY_PATH + apiKey.urlEncode() + - BASE_CURRENCY_PATH + cryptoCurrencyName.urlEncode() + - REFUND_WALLET_ADDRESS_PATH + walletAddress.urlEncode() + - REDIRECT_URL_PATH + REDIRECT_URL_SELL_REQUEST.urlEncode() - } - } - val signature = createSignature(originalQuery, secretKey) - return baseUrl + originalQuery + SIGNATURE_PATH + signature.urlEncode() - - } - - fun getSellCryptoReceiptUrl(transactionId: String): String { - return BASE_URL_SELL + TRANSACTION_RECEIPT_PATH + transactionId - } - - private fun String.urlEncode(): String { - return Uri.encode(this) - } - - private fun createSignature(data: String, key: String): String { - val sha256Hmac = Mac.getInstance("HmacSHA256") - val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256") - sha256Hmac.init(secretKey) - val sha256encoded = sha256Hmac.doFinal(data.toByteArray()) - return Base64.encodeToString(sha256encoded, Base64.NO_WRAP) - } - } - - enum class Action { Buy, Sell } -} 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..dad1a88a3b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -0,0 +1,203 @@ +package com.tangem.tap.features.demo + +import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager +import com.tangem.blockchain.common.* +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.common.CompletionResult +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( +// Tangem Wallet: + "AC01000000041100", + "AC01000000042462", + "AC01000000041647", + "AC01000000041621", + "AC01000000041217", + "AC01000000041225", + "AC01000000041209", + "AC01000000041092", + "AC01000000041472", + "AC01000000041662", + "AC01000000045754", + "AC01000000045960", +// Tangem Note BTC: + "AB0100000046530", + "AB0100000046720", + "AB0100000046746", + "AB0100000046498", + "AB0100000046753", + "AB0100000049608", + "AB0100000046761", + "AB0100000049574", + "AB0100000046605", + "AB0100000046571", + "AB0100000046704", + "AB0100000046647", +// Tangem Note Ethereum: + "AB02000000051000", + "AB02000000050986", + "AB02000000051026", + "AB02000000051042", + "AB02000000051091", + "AB02000000051083", + "AB02000000050960", + "AB02000000051034", + "AB02000000050911", + "AB02000000051133", + "AB02000000051158", + "AB02000000051059", + ) + + 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( + ) +} + +class DemoTransactionSender( + private val walletManager: WalletManager, + private val sender: TransactionSender = walletManager as TransactionSender +) : TransactionSender { + + override suspend fun getFee(amount: Amount, destination: String): Result> { + val blockchain = walletManager.wallet.blockchain + return when (walletManager) { + is BitcoinWalletManager -> Result.Success(listOf( + Amount(0.0001.toBigDecimal(), blockchain), + Amount(0.0003.toBigDecimal(), blockchain), + Amount(0.00055.toBigDecimal(), blockchain), + )) + else -> 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 when (signerResponse) { + is CompletionResult.Success -> SimpleResult.Failure(Exception(ID)) + is CompletionResult.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error) + } + } + + 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..acbe36e4ae 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 @@ -5,6 +5,7 @@ import android.view.View import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater +import by.kirich1409.viewbindingdelegate.viewBinding import com.tangem.tap.common.extensions.getDrawable import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.details.redux.ConfirmScreenState @@ -12,12 +13,14 @@ import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_details_confirm.* +import com.tangem.wallet.databinding.FragmentDetailsConfirmBinding import org.rekotlin.StoreSubscriber class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm), StoreSubscriber { + private val binding: FragmentDetailsConfirmBinding by viewBinding(FragmentDetailsConfirmBinding::bind) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { @@ -47,38 +50,39 @@ class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - toolbar.setNavigationOnClickListener { + binding.toolbar.setNavigationOnClickListener { store.dispatch(NavigationAction.PopBackTo()) } } override fun newState(state: DetailsState) { - if (activity == null) return + if (activity == null || view == null) return + setState(state) + } + private fun setState(state: DetailsState) = with(binding) { when (state.confirmScreenState) { ConfirmScreenState.EraseWallet -> { toolbar.title = getString(R.string.details_row_title_reset_factory_settings) - tv_warning_description.text = getString(R.string.details_row_title_reset_factory_settings_warning) - btn_confirm.text = getString(R.string.details_row_title_reset_factory_settings) - btn_confirm.setCompoundDrawablesRelativeWithIntrinsicBounds( - null, null, getDrawable(R.drawable.ic_send), null + tvWarningDescription.text = getString(R.string.details_row_title_reset_factory_settings_warning) + btnConfirm.text = getString(R.string.details_row_title_reset_factory_settings) + btnConfirm.setCompoundDrawablesRelativeWithIntrinsicBounds( + null, null, getDrawable(R.drawable.ic_send), null ) - btn_confirm.setOnClickListener { store.dispatch(DetailsAction.ResetToFactory.Confirm) } + btnConfirm.setOnClickListener { store.dispatch(DetailsAction.ResetToFactory.Confirm) } } ConfirmScreenState.LongTap, ConfirmScreenState.AccessCode, ConfirmScreenState.PassCode -> { - toolbar.title = getString(R.string.details_row_title_manage_security) - tv_warning_description.text = getString(R.string.details_security_management_warning) - btn_confirm.text = getString(R.string.common_save_changes) - btn_confirm.setCompoundDrawablesRelativeWithIntrinsicBounds( - null, null, getDrawable(R.drawable.ic_save), null + toolbar.title = getString(R.string.details_manage_security_title) + tvWarningDescription.text = getString(R.string.details_security_management_warning) + btnConfirm.text = getString(R.string.common_save_changes) + btnConfirm.setCompoundDrawablesRelativeWithIntrinsicBounds( + null, null, getDrawable(R.drawable.ic_save), null ) - btn_confirm.setOnClickListener { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) } + btnConfirm.setOnClickListener { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) } } } - - } } \ No newline at end of file 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..5df5d5969b 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 @@ -6,6 +6,7 @@ import android.view.View import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater +import by.kirich1409.viewbindingdelegate.viewBinding import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen @@ -18,13 +19,13 @@ import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.feedback.FeedbackEmail import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_details.* -import kotlinx.android.synthetic.main.fragment_wallet.toolbar +import com.tangem.wallet.databinding.FragmentDetailsBinding import org.rekotlin.StoreSubscriber class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber { private var currencySelectionDialog = CurrencySelectionDialog() + private val binding: FragmentDetailsBinding by viewBinding(FragmentDetailsBinding::bind) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -55,15 +56,18 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber R.string.details_manage_security_access_code null -> null } - currentSecurity?.let { tv_security.text = getString(it) } + currentSecurity?.let { tvSecurity.text = getString(it) } if (state.appCurrencyState.showAppCurrencyDialog && !state.appCurrencyState.fiatCurrencies.isNullOrEmpty()) { @@ -147,7 +151,6 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber { + private val binding: FragmentDetailsSecurityBinding by viewBinding( + FragmentDetailsSecurityBinding::bind + ) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { @@ -49,14 +53,14 @@ class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - toolbar.setNavigationOnClickListener { + binding.toolbar.setNavigationOnClickListener { store.dispatch(NavigationAction.PopBackTo()) } setOnClickListeners() } private fun setOnClickListeners() { - btn_save_changes.setOnClickListener { + binding.btnSaveChanges.setOnClickListener { store.state.detailsState.securityScreenState?.selectedOption?.let { store.dispatch(DetailsAction.ManageSecurity.ConfirmSelection(it)) } @@ -64,7 +68,8 @@ class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security), } override fun newState(state: DetailsState) { - if (activity == null) return + if (activity == null || view == null) return + selectSecurityOption(state.securityScreenState?.selectedOption) for (option in SecurityOption.values()) { setupOption( @@ -73,18 +78,18 @@ class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security), ?: EnumSet.noneOf(SecurityOption::class.java) ) } - tv_access_code_unavailable_disclaimer.show( + binding.tvAccessCodeUnavailableDisclaimer.show( state.scanResponse?.card?.backupStatus == Card.BackupStatus.NoBackup ) } - private fun selectSecurityOption(securityOption: SecurityOption?) { - radiobutton_long_tap.isChecked = - securityOption == SecurityOption.LongTap && radiobutton_long_tap.isEnabled - radiobutton_passcode.isChecked = - securityOption == SecurityOption.PassCode && radiobutton_passcode.isEnabled - radiobutton_access_code.isChecked = - securityOption == SecurityOption.AccessCode && radiobutton_access_code.isEnabled + private fun selectSecurityOption(securityOption: SecurityOption?) = with(binding) { + radiobuttonLongTap.isChecked = + securityOption == SecurityOption.LongTap && radiobuttonLongTap.isEnabled + radiobuttonPasscode.isChecked = + securityOption == SecurityOption.PassCode && radiobuttonPasscode.isEnabled + radiobuttonAccessCode.isChecked = + securityOption == SecurityOption.AccessCode && radiobuttonAccessCode.isEnabled } private fun setupOption(option: SecurityOption, allowedOptions: EnumSet) { @@ -95,46 +100,46 @@ class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security), } } - private fun enableLongTap(enable: Boolean) { - group_long_tap.show(enable) + private fun enableLongTap(enable: Boolean) = with(binding) { + groupLongTap.show(enable) val alpha = if (enable) 1f else 0.5f - tv_long_tap_description.alpha = alpha - tv_long_tap_title.alpha = alpha - radiobutton_long_tap.alpha = alpha - radiobutton_long_tap.isEnabled = enable - v_long_tap.isClickable = enable + tvLongTapDescription.alpha = alpha + tvLongTapTitle.alpha = alpha + radiobuttonLongTap.alpha = alpha + radiobuttonLongTap.isEnabled = enable + vLongTap.isClickable = enable if (enable) { - v_long_tap.setOnClickListener { + vLongTap.setOnClickListener { store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.LongTap)) } } } - private fun enablePasscode(enable: Boolean) { - group_passcode.show(enable) + private fun enablePasscode(enable: Boolean) = with(binding) { + groupPasscode.show(enable) val alpha = if (enable) 1f else 0.5f - tv_passcode_description.alpha = alpha - tv_passcode_title.alpha = alpha - radiobutton_passcode.alpha = alpha - radiobutton_passcode.isEnabled = enable - v_passcode.isClickable = enable + tvPasscodeDescription.alpha = alpha + tvPasscodeTitle.alpha = alpha + radiobuttonPasscode.alpha = alpha + radiobuttonPasscode.isEnabled = enable + vPasscode.isClickable = enable if (enable) { - v_passcode.setOnClickListener { + vPasscode.setOnClickListener { store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.PassCode)) } } } - private fun enableAccessCode(enable: Boolean) { - group_access_code.show(enable) + private fun enableAccessCode(enable: Boolean) = with(binding) { + groupAccessCode.show(enable) val alpha = if (enable) 1f else 0.5f - tv_access_code_description.alpha = alpha - tv_access_code_title.alpha = alpha - radiobutton_access_code.alpha = alpha - radiobutton_access_code.isEnabled = enable - v_access_code.isClickable = enable + tvAccessCodeDescription.alpha = alpha + tvAccessCodeTitle.alpha = alpha + radiobuttonAccessCode.alpha = alpha + radiobuttonAccessCode.isEnabled = enable + vAccessCode.isClickable = enable if (enable) { - v_access_code.setOnClickListener { + vAccessCode.setOnClickListener { store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.AccessCode)) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/SessionsAdapter.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/SessionsAdapter.kt index c1028c874d..95c28cf26a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/SessionsAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/SessionsAdapter.kt @@ -3,14 +3,15 @@ package com.tangem.tap.features.details.ui.walletconnect import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.chip.Chip import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.item_wallet_connect_session.view.* class WalletConnectSessionsAdapter : ListAdapter( @@ -41,15 +42,14 @@ class WalletConnectSessionsAdapter RecyclerView.ViewHolder(view) { fun bind(session: WalletConnectSession) { - view.tv_card_id.text = view.context.getString( + view.findViewById(R.id.tv_card_id).text = view.context.getString( R.string.wallet_connect_card_number, session.wallet.cardId ) - view.tv_d_app_name.text = session.peerMeta.name + view.findViewById(R.id.tv_d_app_name).text = session.peerMeta.name - view.btn_disconnect.setOnClickListener { + view.findViewById(R.id.btn_disconnect).setOnClickListener { store.dispatch(WalletConnectAction.DisconnectSession(session.session)) } - } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectSessionsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectSessionsFragment.kt index 1a55a976c7..b019dc95e7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectSessionsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectSessionsFragment.kt @@ -1,19 +1,20 @@ package com.tangem.tap.features.details.ui.walletconnect -import android.app.Dialog import android.os.Bundle import android.view.View import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionInflater +import by.kirich1409.viewbindingdelegate.viewBinding import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_wallet_connect_sessions.* +import com.tangem.wallet.databinding.FragmentWalletConnectSessionsBinding import org.rekotlin.StoreSubscriber @@ -21,7 +22,9 @@ class WalletConnectSessionsFragment : Fragment(R.layout.fragment_wallet_connect_ StoreSubscriber { private lateinit var walletConnectSessionsAdapter: WalletConnectSessionsAdapter - private var dialog: Dialog? = null + private val binding: FragmentWalletConnectSessionsBinding by viewBinding( + FragmentWalletConnectSessionsBinding::bind + ) override fun onCreate(savedInstanceState: Bundle?) { @@ -53,7 +56,7 @@ class WalletConnectSessionsFragment : Fragment(R.layout.fragment_wallet_connect_ override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - toolbar.setNavigationOnClickListener { + binding.toolbar.setNavigationOnClickListener { store.dispatch(NavigationAction.PopBackTo()) } setOnClickListeners() @@ -61,27 +64,31 @@ class WalletConnectSessionsFragment : Fragment(R.layout.fragment_wallet_connect_ } private fun setOnClickListeners() { - fab_open_session.setOnClickListener { + binding.fabOpenSession.setOnClickListener { store.dispatch(WalletConnectAction.StartWalletConnect(activity = requireActivity())) } } - private fun setupTransactionsRecyclerView() { + private fun setupTransactionsRecyclerView() = with(binding) { walletConnectSessionsAdapter = WalletConnectSessionsAdapter() - rv_wallet_connect_sessions.layoutManager = LinearLayoutManager(requireContext()) - rv_wallet_connect_sessions.adapter = walletConnectSessionsAdapter + rvWalletConnectSessions.layoutManager = LinearLayoutManager(requireContext()) + rvWalletConnectSessions.adapter = walletConnectSessionsAdapter walletConnectSessionsAdapter.submitList(store.state.walletConnectState.sessions) } override fun newState(state: WalletConnectState) { - if (activity == null) return + if (activity == null || view == null) return - fab_open_session.show(!state.loading) - pb_wallet_connect.show(state.loading) + binding.fabOpenSession.show(!state.loading) + binding.pbWalletConnect.show(state.loading) - walletConnectSessionsAdapter.submitList(state.sessions) - rv_wallet_connect_sessions.show(state.sessions.isNotEmpty()) - tv_no_sessions.show(state.sessions.isEmpty()) - tv_no_sessions_title.show(state.sessions.isEmpty()) + showUpdatedList(state.sessions) + } + + private fun showUpdatedList(sessions: List) = with(binding) { + walletConnectSessionsAdapter.submitList(sessions) + rvWalletConnectSessions.show(sessions.isNotEmpty()) + tvNoSessions.show(sessions.isEmpty()) + tvNoSessionsTitle.show(sessions.isEmpty()) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index 82f131a3a2..4538be7224 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -5,18 +5,21 @@ import android.view.View import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater +import by.kirich1409.viewbindingdelegate.viewBinding import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.disclaimer.redux.DisclaimerState import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_disclaimer.* +import com.tangem.wallet.databinding.FragmentDisclaimerBinding import org.rekotlin.StoreSubscriber class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer), StoreSubscriber { + private val binding: FragmentDisclaimerBinding by viewBinding(FragmentDisclaimerBinding::bind) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { @@ -45,22 +48,22 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - toolbar.setNavigationOnClickListener { + binding.toolbar.setNavigationOnClickListener { store.dispatch(NavigationAction.PopBackTo()) } setOnClickListeners() } private fun setOnClickListeners() { - btn_accept.setOnClickListener { store.dispatch(DisclaimerAction.AcceptDisclaimer) } + binding.btnAccept.setOnClickListener { store.dispatch(DisclaimerAction.AcceptDisclaimer) } } override fun newState(state: DisclaimerState) { - if (activity == null) return + if (activity == null || view == null) return if (state.accepted) { - half_transparent_overlay.hide() - btn_accept.hide() + binding.halfTransparentOverlay.hide() + binding.btnAccept.hide() } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt index 6da60e3904..34f6cc4807 100644 --- a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt @@ -11,6 +11,9 @@ import com.tangem.common.card.Card import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.domain.TapWorkarounds +import com.tangem.tap.features.feedback.EmailData.Companion.appendBlankLine +import com.tangem.tap.features.feedback.EmailData.Companion.appendDelimiter +import com.tangem.wallet.R import timber.log.Timber import java.io.File import java.io.FileWriter @@ -40,7 +43,8 @@ class FeedbackManager( val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null activity.sendEmail( email = getSupportEmail(), - subject = emailData.subject, message = emailData.joinTogether(infoHolder, emailData !is ScanFailsEmail), + subject = activity.getString(emailData.subjectResId), + message = emailData.joinTogether(activity, infoHolder), file = fileLog, onFail = onFail ) @@ -142,7 +146,7 @@ class AdditionalEmailInfo { cardFirmwareVersion = card.firmwareVersion.stringValue cardIssuer = card.issuer.name signedHashesCount = card.wallets - .joinToString(";") { "${it.curve?.curve} - ${it.totalSignedHashes}" } + .joinToString(";") { "${it.curve?.curve} - ${it.totalSignedHashes}" } } fun setWalletsInfo(walletManagers: List) { @@ -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/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index fae5ee9060..b577781a18 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -2,35 +2,25 @@ package com.tangem.tap.features.home import android.os.Bundle import android.view.View -import androidx.transition.TransitionInflater -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.redux.navigation.FragmentShareTransition -import com.tangem.tap.common.redux.navigation.ShareElement -import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget -import com.tangem.tap.common.toggleWidget.ViewStateWidget -import com.tangem.tap.common.transitions.FrontCardEnterTransition -import com.tangem.tap.common.transitions.FrontCardExitTransition +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_onboarding_main.* -import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.* -import kotlinx.android.synthetic.main.layout_onboarding_home_top.* -import kotlinx.android.synthetic.main.view_bg_home.* +import org.rekotlin.StoreSubscriber -class HomeFragment : BaseOnboardingFragment() { +class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber { - private lateinit var btnScanCard: ViewStateWidget - - override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_home_top + var homeState: MutableState = mutableStateOf(store.state.homeState) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val inflater = TransitionInflater.from(requireContext()) - exitTransition = inflater.inflateTransition(R.transition.fade) store.dispatch(HomeAction.Init) } @@ -39,50 +29,36 @@ class HomeFragment : BaseOnboardingFragment() { store.dispatch(BackupAction.CheckForUnfinishedBackup) - toolbar.hide() - val shareTransition = FragmentShareTransition( - listOf( - ShareElement(imv_front_card, ShareElement.imvFrontCard), - ShareElement(imv_back_card, ShareElement.imvBackCard), - ShareElement(bg_circle_large), - ShareElement(bg_circle_medium), - ShareElement(bg_circle_min), - ), - FrontCardEnterTransition(), - FrontCardExitTransition() - ) - - store.dispatch(HomeAction.SetFragmentShareTransition(shareTransition)) - - tv_header.setText(R.string.home_welcome_header) - tv_body.setText(R.string.home_welcome_body) - - btn_main_action.setText(R.string.home_button_scan) - btn_main_action.setOnClickListener { store.dispatch(HomeAction.ReadCard) } - btn_alternative_action.setText(R.string.home_button_get_new_card) - btn_alternative_action.setOnClickListener { store.dispatch(HomeAction.GoToShop) } - - btnScanCard = IndeterminateProgressButtonWidget(btn_main_action, progress) + getView()?.findViewById(R.id.cv_stories)?.setContent { + AppCompatTheme { + StoriesScreen( + homeState, + onScanButtonClick = { store.dispatch(HomeAction.ReadCard) }, + onShopButtonClick = { store.dispatch(HomeAction.GoToShop) } + ) + } + } } - override fun subscribeToStore() { + override fun onStart() { + super.onStart() store.subscribe(this) { state -> state.skipRepeats { oldState, newState -> oldState.homeState == newState.homeState }.select { it.homeState } } - storeSubscribersList.add(this) } + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun newState(state: HomeState) { - if (activity == null) return + if (activity == null || view == null) return - btnScanCard.changeState(state.btnScanState.progressState) - btnScanCard.mainView.isEnabled = state.btnScanState.enabled + homeState.value = state } - override fun onDestroyView() { - store.dispatch(HomeAction.SetFragmentShareTransition(null)) - super.onDestroyView() - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/AutoSizeText.kt b/app/src/main/java/com/tangem/tap/features/home/compose/AutoSizeText.kt new file mode 100644 index 0000000000..a7d9b2b606 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/AutoSizeText.kt @@ -0,0 +1,45 @@ +package com.tangem.tap.features.home.compose + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.ExperimentalUnitApi + +@OptIn(ExperimentalUnitApi::class) +@Composable +fun AutoSizeText( + text: String, + textStyle: TextStyle, + modifier: Modifier = Modifier +) { + val scaledTextStyle = remember { mutableStateOf(textStyle) } + val readyToDraw = remember { mutableStateOf(false) } + + val onTextLayout: (TextLayoutResult) -> Unit = { result -> + if (result.didOverflowWidth) { + val fontSize = result.size.width / result.multiParagraph.width + scaledTextStyle.value = +// scaledTextStyle.value.copy(fontSize = TextUnit(fontSize, TextUnitType.Sp)) + scaledTextStyle.value.copy(fontSize = scaledTextStyle.value.fontSize * 0.9) + } else { + readyToDraw.value = true + } + } + + Text( + text = text, + modifier = modifier.drawWithContent { + if (readyToDraw.value) { + drawContent() + } + }, + softWrap = false, + onTextLayout = onTextLayout, + style = scaledTextStyle.value, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/FirstStoriesContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/FirstStoriesContent.kt new file mode 100644 index 0000000000..92929c0027 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/FirstStoriesContent.kt @@ -0,0 +1,141 @@ +package com.tangem.tap.features.home.compose + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleIn +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.wallet.R + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun FirstStoriesContent( + paused: Boolean, duration: Int = 8_000, + hideContent: (Boolean) -> Unit +) { + val screenState = remember { mutableStateOf(StartingScreenState.INIT) } + val progress = remember { Animatable(0f) } + + LaunchedEffect(paused) { + if (paused) { + progress.stop() + } else { + progress.animateTo( + targetValue = 2f, + animationSpec = tween( + durationMillis = duration, + easing = LinearEasing + ) + ) + } + } + + when (progress.value) { + in 0f..0.2f -> screenState.value = StartingScreenState.INIT + in 0.2f..0.3f -> screenState.value = StartingScreenState.BUY + in 0.3f..0.4f -> screenState.value = StartingScreenState.STORE + in 0.4f..0.5f -> screenState.value = StartingScreenState.SEND + in 0.5f..0.6f -> screenState.value = StartingScreenState.PAY + in 0.6f..0.7f -> screenState.value = StartingScreenState.EXCHANGE + in 0.7f..0.8f -> screenState.value = StartingScreenState.BORROW + in 0.8f..1f -> screenState.value = StartingScreenState.LEND + in 1f..1.2f -> screenState.value = StartingScreenState.SHOW_CARD + in 1.2f..2f -> screenState.value = StartingScreenState.MEET_TANGEM + } + + if (screenState.value == StartingScreenState.INIT) hideContent(true) + if (screenState.value == StartingScreenState.BUY) hideContent(false) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = + Modifier + .fillMaxSize(), + ) { + + val text = when (screenState.value) { + StartingScreenState.INIT -> null + StartingScreenState.BUY -> R.string.story_meet_buy + StartingScreenState.STORE -> R.string.story_meet_store + StartingScreenState.SEND -> R.string.story_meet_send + StartingScreenState.PAY -> R.string.story_meet_pay + StartingScreenState.EXCHANGE -> R.string.story_meet_exchange + StartingScreenState.BORROW -> R.string.story_meet_borrow + StartingScreenState.LEND -> R.string.story_meet_lend + StartingScreenState.SHOW_CARD -> R.string.story_meet_title + StartingScreenState.MEET_TANGEM -> R.string.story_meet_title + } + + val style = TextStyle( + fontSize = 60.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White, + textAlign = TextAlign.Center, + ) + + if (screenState.value != StartingScreenState.MEET_TANGEM) { + AutoSizeText(text = text?.let { stringResource(text) } ?: "", textStyle = style, + modifier = Modifier + .padding(start = 40.dp, end = 40.dp) + .alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),) + } + + AnimatedVisibility( + visible = screenState.value == StartingScreenState.MEET_TANGEM, + enter = slideInVertically() { it / 2 } + ) { + AutoSizeText(text = text?.let { stringResource(text) } ?: "", textStyle = style, + modifier = Modifier + .padding(start = 40.dp, end = 40.dp) + ) + } + + AnimatedVisibility( + visible = screenState.value == StartingScreenState.SHOW_CARD || + screenState.value == StartingScreenState.MEET_TANGEM, + enter = scaleIn(initialScale = 3f) + ) { + Image( + painter = painterResource( + id = R.drawable.meet_tangem + ), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +enum class StartingScreenState { + INIT, BUY, STORE, SEND, PAY, EXCHANGE, BORROW, LEND, SHOW_CARD, MEET_TANGEM +} + + +@Preview +@Composable +fun Stories1Preview() { + FirstStoriesContent(false, 7_000) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt new file mode 100644 index 0000000000..040c72557f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt @@ -0,0 +1,68 @@ +package com.tangem.tap.features.home.compose + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.wallet.R + +@Composable +fun HomeButtons( + isDarkBackground: Boolean, modifier: Modifier = Modifier, + onScanButtonClick: () -> Unit, + onShopButtonClick: () -> Unit +) { + val darkColorBackground = Color(0xFF26292E) + val darkColorButton = Color(0xFF080C10) + + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = modifier + ) { + Button( + modifier = Modifier + .weight(1f) + .height(48.dp), + onClick = onScanButtonClick, + colors = ButtonDefaults.textButtonColors( + backgroundColor = if (isDarkBackground) darkColorBackground else Color.White, + contentColor = if (isDarkBackground) Color.White else darkColorButton + ) + ) { + Text( + text = stringResource(id = R.string.home_button_scan), + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + textAlign = TextAlign.Center, + maxLines = 1 + ) + } + Spacer(modifier = Modifier.size(8.dp)) + Button( + modifier = Modifier + .weight(1f) + .height(48.dp), + onClick = onShopButtonClick, + colors = ButtonDefaults.textButtonColors( + backgroundColor = if (isDarkBackground) Color.White else darkColorBackground, + contentColor = if (isDarkBackground) darkColorButton else Color.White + ) + ) { + Text( + text = stringResource(id = R.string.home_button_order), + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + textAlign = TextAlign.Center, + maxLines = 1 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt b/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt new file mode 100644 index 0000000000..15728ae713 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt @@ -0,0 +1,157 @@ +package com.tangem.tap.features.home.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.tap.features.home.redux.HomeState +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.wallet.R +import kotlin.math.max + +@Composable +fun StoriesScreen( + homeState: MutableState = mutableStateOf(HomeState()), + onScanButtonClick: () -> Unit, + onShopButtonClick: () -> Unit +) { + val steps = 6 + val stepDuration = 8_000 + + val currentStep = remember { mutableStateOf(1) } + + val isDarkBackground = currentStep.value !in 3..5 + + val goToPreviousScreen = { + currentStep.value = max(1, currentStep.value - 1) + } + val goToNextScreen = { + currentStep.value = if (currentStep.value < steps) currentStep.value + 1 else 1 + } + + val isPressed = remember { mutableStateOf(false) } + val needsToBePaused = + remember(homeState) { mutableStateOf(homeState.value.btnScanState.progressState == ProgressState.Loading) } + val pause = isPressed.value || needsToBePaused.value + + val hideContent = remember { mutableStateOf(true) } + + Box( + Modifier + .fillMaxSize() + .background(Color(0xFF090E13)) + ) { + Row( + Modifier.fillMaxSize() + ) { + Box( + Modifier + .weight(1f) + .fillMaxHeight() + .pointerInput(isPressed) { + detectTapGestures( + onPress = { + val pressStartTime = System.currentTimeMillis() + isPressed.value = true + this.tryAwaitRelease() + val pressEndTime = System.currentTimeMillis() + val totalPressTime = pressEndTime - pressStartTime + if (totalPressTime < 200) goToPreviousScreen() + isPressed.value = false + }, + ) + } + ) + Box( + Modifier.weight(1f) + .fillMaxHeight() + .pointerInput(isPressed) { + detectTapGestures( + onPress = { + val pressStartTime = System.currentTimeMillis() + isPressed.value = true + this.tryAwaitRelease() + val pressEndTime = System.currentTimeMillis() + val totalPressTime = pressEndTime - pressStartTime + if (totalPressTime < 200) goToNextScreen() + isPressed.value = false + }, + ) + } + ) + } + if (!isDarkBackground) { + Image( + painter = painterResource(id = R.drawable.ic_overlay), + contentDescription = null, + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.FillBounds + ) + } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { + Spacer(modifier = Modifier.size(24.dp)) + StoriesProgressBar( + steps = steps, + currentStep = currentStep.value, +// paused = isPressed.value, + stepDuration = stepDuration, + paused = pause, + onStepFinished = goToNextScreen, + ) + Image( + painter = painterResource(id = R.drawable.ic_tangem_logo), + contentDescription = null, + contentScale = ContentScale.FillHeight, + modifier = Modifier + .padding(start = 16.dp, top = 10.dp) + .height(17.dp) + .alpha(if (hideContent.value) 0f else 1f) + .align(Alignment.Start), + colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black) + ) + when (currentStep.value) { + 1 -> FirstStoriesContent(pause, stepDuration) { hideContent.value = it } + 2 -> StoriesRevolutionaryWallet() + 3 -> StoriesUltraSecureBackup() + 4 -> StoriesThousandsOfCurrencies() + 5 -> StoriesWeb3() + 6 -> StoriesWalletForEveryone() + } + } + HomeButtons( + isDarkBackground = isDarkBackground, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(16.dp, 0.dp, 16.dp, 37.dp) + .fillMaxWidth(), + onScanButtonClick = onScanButtonClick, + onShopButtonClick = onShopButtonClick + ) + } +} + + +@Preview +@Composable +fun InstagramScreenPreview() { + StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {}) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt new file mode 100644 index 0000000000..b12be36802 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt @@ -0,0 +1,120 @@ +package com.tangem.tap.features.home.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.wallet.R + +@Composable +fun StoriesGeneralContent( + titleText: String, + subtitleText: String, + imageSource: Int?, + isDarkBackground: Boolean, + imageComposable: (() -> Unit)? = null, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight(), + ) { + + Text( + text = titleText, + fontSize = 32.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier + .padding(start = 40.dp, end = 40.dp), + color = if (isDarkBackground) Color.White else Color(0xFF090E13), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.size(16.dp)) + + Text( + text = subtitleText, + fontSize = 20.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier + .padding(start = 40.dp, end = 40.dp), + color = Color(0xFFA6AAAD), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.size(25.dp)) + + if (imageSource != null) { + Image( + painter = painterResource(id = imageSource), + contentDescription = null, + contentScale = if(isDarkBackground) ContentScale.Inside else ContentScale.FillWidth, + modifier = Modifier.fillMaxWidth() + ) + } + imageComposable?.invoke() + } +} + +@Composable +fun StoriesRevolutionaryWallet() { + StoriesGeneralContent( + titleText = stringResource(id = R.string.story_awe_title), + subtitleText = stringResource(id = R.string.story_awe_description), + imageSource = R.drawable.revolutionary_wallet, + isDarkBackground = true + ) +} + +@Composable +fun StoriesUltraSecureBackup() { + StoriesGeneralContent( + titleText = stringResource(id = R.string.story_backup_title), + subtitleText = stringResource(id = R.string.story_backup_description), + imageSource = R.drawable.floating_cards, + isDarkBackground = false + ) +} + +@Composable +fun StoriesThousandsOfCurrencies() { + StoriesGeneralContent( + titleText = stringResource(id = R.string.story_currencies_title), + subtitleText = stringResource(id = R.string.story_currencies_description), + imageSource = R.drawable.thousands_of_currencies, + isDarkBackground = false + ) +} + +@Composable +fun StoriesWeb3() { + StoriesGeneralContent( + titleText = stringResource(id = R.string.story_web3_title), + subtitleText = stringResource(id = R.string.story_web3_description), + imageSource = R.drawable.web_3, + isDarkBackground = false + ) +} + +@Composable +fun StoriesWalletForEveryone() { + StoriesGeneralContent( + titleText = stringResource(id = R.string.story_finish_title), + subtitleText = stringResource(id = R.string.story_finish_description), + imageSource = R.drawable.wallet_for_everyone, + isDarkBackground = true + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt new file mode 100644 index 0000000000..b8dd83e070 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt @@ -0,0 +1,80 @@ +package com.tangem.tap.features.home.compose + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun StoriesProgressBar( + steps: Int, + currentStep: Int, + paused: Boolean = false, + stepDuration: Int = 8_000, + onStepFinished: () -> Unit = {}, +) { + val progress = remember(currentStep) { Animatable(0f) } + + LaunchedEffect(paused, currentStep) { + if (paused) { + progress.stop() + } else { + progress.animateTo( + targetValue = 1f, + animationSpec = tween( + durationMillis = (stepDuration * (1f - progress.value)).toInt(), + easing = LinearEasing + ) + ) + progress.snapTo(0f) + onStepFinished() + } + + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier +// .height() + .padding(start = 9.dp, end = 9.dp, top = 16.dp), + ) { + for (index in 1..steps) { + Row( + modifier = Modifier + .height(2.dp) + .weight(1f) + .background(Color.White.copy(alpha = 0.4f)) + ) { + Box( + modifier = Modifier + .background(Color.White) + .fillMaxHeight().let { + when (index) { + currentStep -> it.fillMaxWidth(progress.value) + in 0..currentStep -> it.fillMaxWidth(1f) + else -> it + } + }, + ) {} + } + if (index != steps) { + Spacer(modifier = Modifier.width(4.dp)) + } + } + } +} + +@Preview +@Composable +fun StoriesProgressBarPreview() { + StoriesProgressBar(steps = 3, currentStep = 2, paused = false) { } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index 14f762f6c4..d14d4cf71b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -1,7 +1,6 @@ package com.tangem.tap.features.home.redux import com.tangem.tap.common.entities.IndeterminateProgressButton -import com.tangem.tap.common.redux.navigation.FragmentShareTransition import org.rekotlin.Action sealed class HomeAction : Action { @@ -10,10 +9,9 @@ sealed class HomeAction : Action { object GoToShop : HomeAction() // internal - data class ShouldScanCardOnResume(val shouldScanCard: Boolean):HomeAction() + data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction() object Init : HomeAction() - data class SetFragmentShareTransition(val shareTransition: FragmentShareTransition?) : HomeAction() data class SetTermsOfUseState(val isDisclaimerAccepted: Boolean) : HomeAction() data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction() 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..89c34cbf42 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 @@ -39,7 +38,7 @@ private val homeMiddleware: Middleware = { dispatch, state -> when (action) { is HomeAction.Init -> { store.dispatch(GlobalAction.RestoreAppCurrency) - store.dispatch(GlobalAction.GetMoonPayStatus) + store.dispatch(GlobalAction.InitCurrencyExchangeManager) store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted())) } is HomeAction.ShouldScanCardOnResume -> { @@ -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) @@ -74,14 +73,14 @@ private fun handleReadCard() { if (OnboardingHelper.isOnboardingCase(scanResponse)) { val navigateTo = OnboardingHelper.whereToNavigate(scanResponse) store.dispatch(GlobalAction.Onboarding.Start(scanResponse)) - navigateTo(navigateTo, store.state.homeState.shareTransition) + navigateTo(navigateTo) } else { scope.launch { store.onCardScanned(scanResponse) withMainContext { if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly)) - navigateTo(AppScreen.OnboardingTwins, store.state.homeState.shareTransition) + navigateTo(AppScreen.OnboardingTwins) } else { navigateTo(AppScreen.Wallet, null) } @@ -99,7 +98,7 @@ private fun changeButtonState(state: ButtonState) { store.dispatch(HomeAction.ChangeScanCardButtonState(btnState)) } -private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition?) { +private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition? = null) { post(DELAY_SDK_DIALOG_CLOSE) { changeButtonState(ButtonState.ENABLED) store.dispatch(NavigationAction.NavigateTo(screen, transition)) diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt index d4f11f31f9..f4a7a9d407 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt @@ -15,9 +15,6 @@ private fun internalReduce(action: Action, state: AppState): HomeState { var state = state.homeState when (action) { - is HomeAction.SetFragmentShareTransition -> { - state = state.copy(shareTransition = action.shareTransition) - } is HomeAction.ShouldScanCardOnResume -> { state = state.copy(shouldScanCardOnResume = action.shouldScanCard) } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt index 8c9949cf4c..38b19013d8 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt @@ -1,12 +1,10 @@ package com.tangem.tap.features.home.redux import com.tangem.tap.common.entities.IndeterminateProgressButton -import com.tangem.tap.common.redux.navigation.FragmentShareTransition import com.tangem.tap.features.send.redux.states.ButtonState import org.rekotlin.StateType data class HomeState( val shouldScanCardOnResume: Boolean = false, - val shareTransition: FragmentShareTransition? = null, val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED), ) : StateType \ No newline at end of file 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..e9f47acef9 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 @@ -1,6 +1,8 @@ package com.tangem.tap.features.onboarding import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.tangem.tap.common.extensions.copyToClipboard @@ -8,11 +10,11 @@ import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchShare import com.tangem.tap.common.extensions.dispatchToastNotification import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.features.wallet.redux.AddressData import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.dialog_onboarding_address_info.* -import kotlinx.android.synthetic.main.layout_pseudo_toolbar.* +import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding /** [REDACTED_AUTHOR] @@ -22,30 +24,40 @@ class AddressInfoBottomSheetDialog( context: Context ) : BottomSheetDialog(context) { - init { - this.setContentView(R.layout.dialog_onboarding_address_info) + var binding: DialogOnboardingAddressInfoBinding? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DialogOnboardingAddressInfoBinding + .inflate(LayoutInflater.from(context)) + setContentView(binding!!.root) behavior.state = BottomSheetBehavior.STATE_EXPANDED - setOnCancelListener { store.dispatchDialogHide() } + setOnCancelListener { + store.dispatchDialogHide() + binding = null + } } override fun show() { super.show() - val data = stateDialog.addressData + showData(data = stateDialog.addressData) + } - imv_close.setOnClickListener { + private fun showData(data: AddressData) = with(binding!!) { + pseudoToolbar.imvClose.setOnClickListener { dismissWithAnimation = true cancel() } - imv_qr_code.setImageBitmap(data.qrCode) - tv_address.text = data.address - btn_fl_copy_address.setOnClickListener { + imvQrCode.setImageBitmap(data.qrCode) + tvAddress.text = data.address + btnFlCopyAddress.setOnClickListener { context.copyToClipboard(data.address) store.dispatchToastNotification(R.string.copy_toast_msg) } - btn_fl_share.setOnClickListener { + btnFlShare.setOnClickListener { store.dispatchShare(data.shareUrl) } - tv_recieve_message.text = getQRReceiveMessage(tv_recieve_message.context, stateDialog.currency) + tvReceiveMessage.text = getQRReceiveMessage(tvReceiveMessage.context, stateDialog.currency) } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt index b33b3118f5..f29444abc5 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt @@ -1,14 +1,12 @@ package com.tangem.tap.features.onboarding.products -import android.os.Bundle -import android.view.View -import com.tangem.tap.common.extensions.inflate +import by.kirich1409.viewbindingdelegate.viewBinding import com.tangem.tap.common.extensions.show import com.tangem.tap.common.transitions.HomeToOnboardingTransition import com.tangem.tap.features.BaseStoreFragment import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_onboarding_main.* -import kotlinx.android.synthetic.main.view_confetti.* +import com.tangem.wallet.databinding.FragmentOnboardingMainBinding +import com.tangem.wallet.databinding.ViewOnboardingProgressBinding import org.rekotlin.StoreSubscriber /** @@ -16,26 +14,22 @@ import org.rekotlin.StoreSubscriber */ abstract class BaseOnboardingFragment : BaseStoreFragment(R.layout.fragment_onboarding_main), StoreSubscriber { + protected val binding: FragmentOnboardingMainBinding by viewBinding(FragmentOnboardingMainBinding::bind) + protected val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind) + override fun configureTransitions() { enterTransition = HomeToOnboardingTransition() exitTransition = HomeToOnboardingTransition() } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - onboarding_top_container.inflate(getOnboardingTopContainerId(), true) - } - - abstract fun getOnboardingTopContainerId(): Int - - protected fun showConfetti(show: Boolean) { - lav_confetti.show(show) + protected fun showConfetti(show: Boolean) = with(binding.vConfetti) { + lavConfetti.show(show) if (show) { - lav_confetti.playAnimation() + lavConfetti.playAnimation() } else { - lav_confetti.cancelAnimation() + lavConfetti.cancelAnimation() } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt index d9d832d5a6..3125784ba9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt @@ -26,11 +26,6 @@ import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteStat import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteStep import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_onboarding_main.* -import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.* -import kotlinx.android.synthetic.main.layout_onboarding_container_top.* -import kotlinx.android.synthetic.main.view_onboarding_progress.* -import kotlinx.android.synthetic.main.view_onboarding_tv_balance.* /** [REDACTED_AUTHOR] @@ -41,8 +36,6 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget - override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) postponeEnterTransition() @@ -52,11 +45,11 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { super.onViewCreated(view, savedInstanceState) addBackPressHandler(this) - imv_front_card.transitionName = ShareElement.imvFrontCard + binding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard startPostponedEnterTransition() - toolbar.setTitle(R.string.onboarding_title) - btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container) + binding.toolbar.setTitle(R.string.onboarding_title) + btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer) store.dispatch(OnboardingNoteAction.LoadCardArtwork) store.dispatch(OnboardingNoteAction.DetermineStepOfScreen) @@ -72,19 +65,19 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } override fun newState(state: OnboardingNoteState) { - if (activity == null) return + if (activity == null || view == null) return Picasso.get() - .load(state.cardArtworkUrl) - .error(R.drawable.card_placeholder_black) - .placeholder(R.drawable.card_placeholder_black) - ?.into(imv_front_card) + .load(state.cardArtworkUrl) + .error(R.drawable.card_placeholder_black) + .placeholder(R.drawable.card_placeholder_black) + ?.into(binding.onboardingTopContainer.imvFrontCard) - pb_state.max = state.steps.size - 1 - pb_state.progress = state.progress + pbBinding.pbState.max = state.steps.size - 1 + pbBinding.pbState.progress = state.progress when (state.currentStep) { - OnboardingNoteStep.None -> setupNoneState(state) + OnboardingNoteStep.None -> setupNoneState() OnboardingNoteStep.CreateWallet -> setupCreateWalletState(state) OnboardingNoteStep.TopUpWallet -> setupTopUpWalletState(state) OnboardingNoteStep.Done -> setupDoneState(state) @@ -97,77 +90,82 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { private fun setBalance(state: OnboardingNoteState) { if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return - if (state.balanceCriticalError == null) { - val balanceValue = state.walletBalance.value.stripZeroPlainString() - tv_balance_value.text = balanceValue - tv_balance_currency.text = state.walletBalance.currency.currencySymbol - } else { - tv_balance_value.text = "–" - tv_balance_currency.text = "" + with (binding.onboardingTopContainer.onboardingTvBalance) { + if (state.balanceCriticalError == null) { + val balanceValue = state.walletBalance.value.stripZeroPlainString() + tvBalanceValue.text = balanceValue + tvBalanceCurrency.text = state.walletBalance.currency.currencySymbol + } else { + tvBalanceValue.text = "–" + tvBalanceCurrency.text = "" + } } } - private fun setupNoneState(state: OnboardingNoteState) { - btn_main_action.isVisible = false - btn_alternative_action.isVisible = false + private fun setupNoneState() = with(binding.onboardingActionContainer) { + btnMainAction.isVisible = false + btnAlternativeAction.isVisible = false btnRefreshBalanceWidget.show(false) - tv_header.isVisible = false - tv_body.isVisible = false + tvHeader.isVisible = false + tvBody.isVisible = false - btn_main_action.text = "" - btn_alternative_action.text = "" - tv_header.text = "" - tv_body.text = "" + btnMainAction.text = "" + btnAlternativeAction.text = "" + tvHeader.text = "" + tvBody.text = "" } - private fun setupCreateWalletState(state: OnboardingNoteState) { - btn_main_action.setText(R.string.onboarding_create_wallet_button_create_wallet) - btn_main_action.setOnClickListener { store.dispatch(OnboardingNoteAction.CreateWallet) } - btn_alternative_action.setText(R.string.onboarding_button_what_does_it_mean) - btn_alternative_action.setOnClickListener { } + private fun setupCreateWalletState(state: OnboardingNoteState) = + with(binding.onboardingActionContainer) { + btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) + btnMainAction.setOnClickListener { store.dispatch(OnboardingNoteAction.CreateWallet) } + btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean) + btnAlternativeAction.setOnClickListener { } - btnRefreshBalanceWidget.mainView.setOnClickListener(null) + btnRefreshBalanceWidget.mainView.setOnClickListener(null) - tv_header.setText(R.string.onboarding_create_wallet_header) - tv_body.setText(R.string.onboarding_create_wallet_body) + tvHeader.setText(R.string.onboarding_create_wallet_header) + tvBody.setText(R.string.onboarding_create_wallet_body) - imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_circle)) - updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet) + binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( + requireContext().getDrawableCompat(R.drawable.shape_circle) + ) + updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet) - btn_alternative_action.isVisible = false // temporary - } + btnAlternativeAction.isVisible = false // temporary + } - private fun setupTopUpWalletState(state: OnboardingNoteState) { + private fun setupTopUpWalletState(state: OnboardingNoteState) = with(binding.onboardingActionContainer) { if (state.isBuyAllowed) { - btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto) - btn_main_action.setOnClickListener { + btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) + btnMainAction.setOnClickListener { store.dispatch(OnboardingNoteAction.TopUp) } - btn_alternative_action.isVisible = true - btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address) - btn_alternative_action.setOnClickListener { + btnAlternativeAction.isVisible = true + btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address) + btnAlternativeAction.setOnClickListener { store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog) } } else { - btn_main_action.setText(R.string.onboarding_button_receive_crypto) - btn_main_action.setOnClickListener { + btnMainAction.setText(R.string.onboarding_button_receive_crypto) + btnMainAction.setOnClickListener { store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog) } - btn_alternative_action.isVisible = false + btnAlternativeAction.isVisible = false } - tv_header.setText(R.string.onboarding_top_up_header) + tvHeader.setText(R.string.onboarding_top_up_header) if (state.balanceNonCriticalError == null) { - tv_body.setText(R.string.onboarding_top_up_body) + tvBody.setText(R.string.onboarding_top_up_body) } else { state.walletBalance.amountToCreateAccount?.let { amount -> val tvBodyMessage = getString( R.string.onboarding_top_up_body_no_account_error, amount, state.walletBalance.currency.currencySymbol ) - tv_body.text = tvBodyMessage + tvBody.text = tvBodyMessage } } @@ -177,38 +175,43 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { store.dispatch(OnboardingNoteAction.Balance.Update) } } - imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)) + binding.onboardingTopContainer.imvCardBackground + .setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)) updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet) } - private fun setupDoneState(state: OnboardingNoteState) { - btn_main_action.setText(R.string.onboarding_done_button_continue) - btn_main_action.setOnClickListener { + private fun setupDoneState(state: OnboardingNoteState) = with(binding.onboardingActionContainer) { + btnMainAction.setText(R.string.onboarding_done_button_continue) + btnMainAction.setOnClickListener { showConfetti(false) store.dispatch(OnboardingNoteAction.Done) } - btn_alternative_action.isVisible = false - btn_alternative_action.setText("") - btn_alternative_action.setOnClickListener { } + btnAlternativeAction.isVisible = false + btnAlternativeAction.setText("") + btnAlternativeAction.setOnClickListener { } btnRefreshBalanceWidget.mainView.setOnClickListener(null) - tv_header.setText(R.string.onboarding_done_header) - tv_body.setText(R.string.onboarding_done_body) + tvHeader.setText(R.string.onboarding_done_header) + tvBody.setText(R.string.onboarding_done_body) - imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)) + binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( + requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8) + ) updateConstraints(state.currentStep, R.layout.lp_onboarding_done_activation) } private fun updateConstraints(currentStep: OnboardingNoteStep, @LayoutRes layoutId: Int) { if (this.previousStep == currentStep) return - val constraintSet = ConstraintSet() - constraintSet.clone(requireContext(), layoutId) - constraintSet.applyTo(onboarding_main_container) - val transition = InternalNoteLayoutTransition() - transition.interpolator = OvershootInterpolator() - TransitionManager.beginDelayedTransition(onboarding_main_container, transition) + with(binding.onboardingTopContainer) { + val constraintSet = ConstraintSet() + constraintSet.clone(requireContext(), layoutId) + constraintSet.applyTo(onboardingMainContainer) + val transition = InternalNoteLayoutTransition() + transition.interpolator = OvershootInterpolator() + TransitionManager.beginDelayedTransition(onboardingMainContainer, transition) + } } } 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/note/redux/OnboardingNoteState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt index 150c551f0b..77b12445f7 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt @@ -27,7 +27,7 @@ data class OnboardingNoteState( get() = steps.indexOf(currentStep) val isBuyAllowed: Boolean by ReadOnlyProperty { thisRef, property -> - store.state.globalState.moonpayStatus?.buyIsAllowed(walletBalance.currency) ?: false + store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt index 22e479721b..996fdd67fe 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt @@ -18,18 +18,12 @@ import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOt import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsStep import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_onboarding_main.* -import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.* -import kotlinx.android.synthetic.main.layout_onboarding_container_top.* -import kotlinx.android.synthetic.main.view_onboarding_progress.* /** [REDACTED_AUTHOR] */ class OnboardingOtherCardsFragment : BaseOnboardingFragment() { - override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) postponeEnterTransition() @@ -39,10 +33,10 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment setupCreateWalletState(state) - OnboardingOtherCardsStep.Done -> setupDoneState(state) + OnboardingOtherCardsStep.CreateWallet -> setupCreateWalletState() + OnboardingOtherCardsStep.Done -> setupDoneState() } showConfetti(state.showConfetti) } - private fun setupCreateWalletState(state: OnboardingOtherCardsState) { - btn_main_action.setText(R.string.onboarding_create_wallet_button_create_wallet) - btn_main_action.setOnClickListener { store.dispatch(OnboardingOtherCardsAction.CreateWallet) } - btn_alternative_action.setText(R.string.onboarding_button_what_does_it_mean) - btn_alternative_action.setOnClickListener { } + private fun setupCreateWalletState() = with(binding) { - tv_header.setText(R.string.onboarding_create_wallet_header) - tv_body.setText(R.string.onboarding_create_wallet_body) + with(onboardingActionContainer) { + btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) + btnMainAction.setOnClickListener { store.dispatch(OnboardingOtherCardsAction.CreateWallet) } + btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean) + btnAlternativeAction.setOnClickListener { } - imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_circle)) + tvHeader.setText(R.string.onboarding_create_wallet_header) + tvBody.setText(R.string.onboarding_create_wallet_body) + + btnAlternativeAction.isVisible = false // temporary + } + + + binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( + requireContext().getDrawableCompat(R.drawable.shape_circle) + ) updateConstraints(R.layout.lp_onboarding_create_wallet) - btn_alternative_action.isVisible = false // temporary } - private fun setupDoneState(state: OnboardingOtherCardsState) { - btn_main_action.setText(R.string.onboarding_done_button_continue) - btn_main_action.setOnClickListener { + private fun setupDoneState() = with(binding.onboardingActionContainer) { + btnMainAction.setText(R.string.onboarding_done_button_continue) + btnMainAction.setOnClickListener { showConfetti(false) store.dispatch(OnboardingOtherCardsAction.Done) } - btn_alternative_action.isVisible = false - btn_alternative_action.setText("") - btn_alternative_action.setOnClickListener { } + btnAlternativeAction.isVisible = false + btnAlternativeAction.setText("") + btnAlternativeAction.setOnClickListener { } - tv_header.setText(R.string.onboarding_done_header) - tv_body.setText(R.string.onboarding_done_body) + tvHeader.setText(R.string.onboarding_done_header) + tvBody.setText(R.string.onboarding_done_body) - imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)) + binding.onboardingTopContainer.imvCardBackground?.setBackgroundDrawable( + requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8) + ) updateConstraints(R.layout.lp_onboarding_done) } private fun updateConstraints(@LayoutRes layoutId: Int) { - val constraintSet = ConstraintSet() - constraintSet.clone(requireContext(), layoutId) - constraintSet.applyTo(onboarding_main_container) - val transition = InternalNoteLayoutTransition() - transition.interpolator = OvershootInterpolator() - TransitionManager.beginDelayedTransition(onboarding_main_container, transition) + with(binding.onboardingTopContainer) { + val constraintSet = ConstraintSet() + constraintSet.clone(requireContext(), layoutId) + constraintSet.applyTo(onboardingMainContainer) + val transition = InternalNoteLayoutTransition() + transition.interpolator = OvershootInterpolator() + TransitionManager.beginDelayedTransition(onboardingMainContainer, transition) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt index 9d68635c08..eb05dd7402 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt @@ -57,7 +57,7 @@ data class TwinCardsState( get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet val isBuyAllowed: Boolean by ReadOnlyProperty { thisRef, property -> - store.state.globalState.moonpayStatus?.buyIsAllowed(walletBalance.currency) ?: false + store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false } } 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..b1c97b64ee 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 @@ -31,12 +32,7 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep import com.tangem.tap.features.wallet.redux.Artwork import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_onboarding_main.* -import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.* -import kotlinx.android.synthetic.main.layout_onboarding_container_top.* -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.* +import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding class TwinsCardsFragment : BaseOnboardingFragment() { @@ -47,12 +43,11 @@ class TwinsCardsFragment : BaseOnboardingFragment() { private val assetReader: AssetReader by lazy { object : AssetReader { - override fun readAssetAsString(name: String): String = requireContext().readAssetAsString(name) + override fun readAssetAsString(name: String): String = + requireContext().readAssetAsString(name) } } - override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) postponeEnterTransition() @@ -74,36 +69,44 @@ class TwinsCardsFragment : BaseOnboardingFragment() { super.onViewCreated(view, savedInstanceState) addBackPressHandler(this) - reconfigureLayoutForTwins() - twinsWidget = TwinsCardWidget(LeapfrogWidget(cards_container)) { 285f } - btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container) + reconfigureLayoutForTwins(binding.onboardingTopContainer) - toolbar.title = getText(R.string.twins_recreate_toolbar) + val typedValue = TypedValue() + resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true) + val deviceScaleFactorForWelcomeState = typedValue.float + + twinsWidget = TwinsCardWidget(LeapfrogWidget(binding.onboardingTopContainer.cardsContainer), deviceScaleFactorForWelcomeState) { + 285f * deviceScaleFactorForWelcomeState + } + btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer) + + binding.toolbar.title = getText(R.string.twins_recreate_toolbar) Picasso.get() - .load(Artwork.TWIN_CARD_1) - .error(R.drawable.card_placeholder_black) - .placeholder(R.drawable.card_placeholder_black) - ?.into(imv_twin_front_card) + .load(Artwork.TWIN_CARD_1) + .error(R.drawable.card_placeholder_black) + .placeholder(R.drawable.card_placeholder_black) + ?.into(binding.onboardingTopContainer.imvTwinFrontCard) Picasso.get() - .load(Artwork.TWIN_CARD_2) - .error(R.drawable.card_placeholder_white) - .placeholder(R.drawable.card_placeholder_white) - ?.into(imv_twin_back_card) + .load(Artwork.TWIN_CARD_2) + .error(R.drawable.card_placeholder_white) + .placeholder(R.drawable.card_placeholder_white) + ?.into(binding.onboardingTopContainer.imvTwinBackCard) } - private fun reconfigureLayoutForTwins() { - imv_front_card.hide() - imv_card_background.hide() - cards_container.show() + private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) = + with(containerBinding) { + imvFrontCard.hide() + imvCardBackground.hide() + cardsContainer.show() - imv_twin_front_card.transitionName = ShareElement.imvFrontCard - imv_twin_back_card.transitionName = ShareElement.imvBackCard + imvTwinFrontCard.transitionName = ShareElement.imvFrontCard + imvTwinBackCard.transitionName = ShareElement.imvBackCard - // if don't this, the bg_circle_... is overflow the app_bar - app_bar.bringToFront() - } + // if don't this, the bg_circle_... is overflow the app_bar + binding.appBar.bringToFront() + } override fun subscribeToStore() { store.subscribe(this) { state -> @@ -115,10 +118,10 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } override fun newState(state: TwinCardsState) { - if (activity == null) return + if (activity == null || view == null) return - pb_state.max = state.steps.size - 1 - pb_state.progress = state.progress + pbBinding.pbState.max = state.steps.size - 1 + pbBinding.pbState.progress = state.progress when (state.currentStep) { TwinCardsStep.WelcomeOnly -> setupWelcomeOnlyState(state) @@ -139,13 +142,15 @@ class TwinsCardsFragment : BaseOnboardingFragment() { private fun setBalance(state: TwinCardsState) { if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return - if (state.balanceCriticalError == null) { - val balanceValue = state.walletBalance.value.stripZeroPlainString() - tv_balance_value.text = balanceValue - tv_balance_currency.text = state.walletBalance.currency.currencySymbol - } else { - tv_balance_value.text = "–" - tv_balance_currency.text = "" + with(binding.onboardingTopContainer.onboardingTvBalance) { + if (state.balanceCriticalError == null) { + val balanceValue = state.walletBalance.value.stripZeroPlainString() + tvBalanceValue.text = balanceValue + tvBalanceCurrency.text = state.walletBalance.currency.currencySymbol + } else { + tvBalanceValue.text = "–" + tvBalanceCurrency.text = "" + } } } @@ -160,203 +165,235 @@ class TwinsCardsFragment : BaseOnboardingFragment() { setupWelcomeState(state) { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet)) } } - private fun setupWelcomeState(state: TwinCardsState, mainAction: VoidCallback) { + private fun setupWelcomeState(state: TwinCardsState, mainAction: VoidCallback) = + with(binding.onboardingActionContainer) { + twinsWidget.toWelcome(false) { startPostponedEnterTransition() } + + binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.show() + pbBinding.pbState.hide() + + tvHeader.setText(R.string.twins_onboarding_subtitle) + tvBody.text = getString( + R.string.twins_onboarding_description_format, + state.cardNumber?.pairIndexNumber() + ) + + btnMainAction.setText(R.string.common_continue) + btnMainAction.setOnClickListener { mainAction() } + } + + private fun setupWarningState(state: TwinCardsState) = with(binding.onboardingActionContainer) { twinsWidget.toWelcome(false) { startPostponedEnterTransition() } - onboarding_twins_welcome_bg.show() - pb_state.hide() + binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() + pbBinding.pbState.hide() + chbUnderstand.show() - tv_header.setText(R.string.twins_onboarding_subtitle) - tv_body.text = getString(R.string.twins_onboarding_description_format, state.cardNumber?.pairIndexNumber()) + tvHeader.setText(R.string.common_warning) + tvBody.setText(R.string.twins_recreate_warning) - btn_main_action.setText(R.string.common_continue) - btn_main_action.setOnClickListener { mainAction() } - } - - private fun setupWarningState(state: TwinCardsState) { - twinsWidget.toWelcome(false) { startPostponedEnterTransition() } - - onboarding_twins_welcome_bg.hide() - pb_state.hide() - chb_understand.show() - - tv_header.setText(R.string.common_warning) - tv_body.setText(R.string.twins_recreate_warning) - - chb_understand.setOnCheckedChangeListener { buttonView, isChecked -> + chbUnderstand.setOnCheckedChangeListener { buttonView, isChecked -> store.dispatch(TwinCardsAction.SetUserUnderstand(isChecked)) } - btn_main_action.isEnabled = state.userWasUnderstandIfWalletRecreate - btn_main_action.setText(R.string.common_continue) - btn_main_action.setOnClickListener { + btnMainAction.isEnabled = state.userWasUnderstandIfWalletRecreate + btnMainAction.setText(R.string.common_continue) + btnMainAction.setOnClickListener { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet)) } } - private fun setupCreateFirstWalletState(state: TwinCardsState) { - onboarding_twins_welcome_bg.hide() - bg_circle_large.hide() - bg_circle_medium.hide() - bg_circle_min.hide() - chb_understand.hide() - pb_state.show() + private fun setupCreateFirstWalletState(state: TwinCardsState) = + with(binding.onboardingActionContainer) { + binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() + binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleLarge.hide() + binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMedium.hide() + binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMin.hide() + chbUnderstand.hide() + pbBinding.pbState.show() - onboarding_main_container.beginDelayedTransition() + binding.onboardingTopContainer.onboardingMainContainer.beginDelayedTransition() - when (previousStep) { - TwinCardsStep.None -> { - twinsWidget.toLeapfrog(false) { - when (state.cardNumber) { - TwinCardNumber.First -> startPostponedEnterTransition() - TwinCardNumber.Second -> { - switchToCard(state.cardNumber, false) { startPostponedEnterTransition() } + when (previousStep) { + TwinCardsStep.None -> { + twinsWidget.toLeapfrog(false) { + when (state.cardNumber) { + TwinCardNumber.First -> startPostponedEnterTransition() + TwinCardNumber.Second -> { + switchToCard( + state.cardNumber, + false + ) { startPostponedEnterTransition() } + } } } } + TwinCardsStep.Welcome, TwinCardsStep.Warning -> { + twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) }) + } } - TwinCardsStep.Welcome, TwinCardsStep.Warning -> { - twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) }) + + val twinIndexNumber = state.cardNumber?.indexNumber() + tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) + tvBody.setText(R.string.onboarding_twins_interrupt_warning) + + btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) + btnMainAction.setOnClickListener { + store.dispatch( + TwinCardsAction.Wallet.LaunchFirstStep( + Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), + assetReader + ) + ) + } + + btnAlternativeAction.isVisible = false + btnAlternativeAction.isClickable = false + } + + private fun setupCreateSecondWalletState(state: TwinCardsState) = + with(binding.onboardingActionContainer) { + switchToCard(state.cardNumber?.pairNumber()) + + val twinPairIndexNumber = state.cardNumber?.pairIndexNumber() + tvHeader.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber) + tvBody.setText(R.string.onboarding_twins_interrupt_warning) + + btnMainAction.text = + getString(R.string.twins_recreate_button_format, twinPairIndexNumber) + btnMainAction.setOnClickListener { + store.dispatch( + TwinCardsAction.Wallet.LaunchSecondStep( + Message( + getString( + R.string.twins_recreate_title_format, + twinPairIndexNumber + ) + ), + Message(getString(R.string.twins_recreate_title_preparing)), + Message(getString(R.string.twins_recreate_title_creating_wallet)), + ) + ) } } - val twinIndexNumber = state.cardNumber?.indexNumber() - tv_header.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) - tv_body.setText(R.string.onboarding_twins_interrupt_warning) + private fun setupCreateThirdWalletState(state: TwinCardsState) = + with(binding.onboardingActionContainer) { + switchToCard(state.cardNumber) - btn_main_action.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) - btn_main_action.setOnClickListener { - store.dispatch(TwinCardsAction.Wallet.LaunchFirstStep( - Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), - assetReader - )) + val twinIndexNumber = state.cardNumber?.indexNumber() + tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) + tvBody.setText(R.string.onboarding_twins_interrupt_warning) + + btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) + btnMainAction.setOnClickListener { + store.dispatch( + TwinCardsAction.Wallet.LaunchThirdStep( + Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)) + ) + ) + } } - btn_alternative_action.isVisible = false - btn_alternative_action.isClickable = false - } - private fun setupCreateSecondWalletState(state: TwinCardsState) { - switchToCard(state.cardNumber?.pairNumber()) - - val twinPairIndexNumber = state.cardNumber?.pairIndexNumber() - tv_header.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber) - tv_body.setText(R.string.onboarding_twins_interrupt_warning) - - btn_main_action.text = getString(R.string.twins_recreate_button_format, twinPairIndexNumber) - btn_main_action.setOnClickListener { - store.dispatch(TwinCardsAction.Wallet.LaunchSecondStep( - Message(getString(R.string.twins_recreate_title_format, twinPairIndexNumber)), - Message(getString(R.string.twins_recreate_title_preparing)), - Message(getString(R.string.twins_recreate_title_creating_wallet)), - )) - } - } - - private fun setupCreateThirdWalletState(state: TwinCardsState) { - switchToCard(state.cardNumber) - - val twinIndexNumber = state.cardNumber?.indexNumber() - tv_header.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) - tv_body.setText(R.string.onboarding_twins_interrupt_warning) - - btn_main_action.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) - btn_main_action.setOnClickListener { - store.dispatch(TwinCardsAction.Wallet.LaunchThirdStep( - Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)) - )) - } - } - - - private fun setupTopUpWalletState(state: TwinCardsState) { - when (previousStep) { - TwinCardsStep.None -> { - when (state.cardNumber) { - TwinCardNumber.First -> { - twinsWidget.leapfrogWidget.unfold(false) { - twinsWidget.toActivate(false) { + private fun setupTopUpWalletState(state: TwinCardsState) = + with(binding.onboardingActionContainer) { + when (previousStep) { + TwinCardsStep.None -> { + when (state.cardNumber) { + TwinCardNumber.First -> { + twinsWidget.leapfrogWidget.unfold(false) { + twinsWidget.toActivate(false) { + startPostponedEnterTransition() + } + } + } + TwinCardNumber.Second -> { + switchToCard(state.cardNumber, false) { + twinsWidget.toActivate(false) startPostponedEnterTransition() } } } - TwinCardNumber.Second -> { - switchToCard(state.cardNumber, false) { - twinsWidget.toActivate(false) - startPostponedEnterTransition() - } - } + } + TwinCardsStep.CreateThirdWallet -> { + twinsWidget.toActivate() } } - TwinCardsStep.CreateThirdWallet -> { - twinsWidget.toActivate() - } - } - if (state.isBuyAllowed) { - btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto) - btn_main_action.setOnClickListener { - store.dispatch(TwinCardsAction.TopUp) + if (state.isBuyAllowed) { + btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) + btnMainAction.setOnClickListener { + store.dispatch(TwinCardsAction.TopUp) + } + + btnAlternativeAction.isVisible = true + btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address) + btnAlternativeAction.setOnClickListener { + store.dispatch(TwinCardsAction.ShowAddressInfoDialog) + } + } else { + btnMainAction.setText(R.string.onboarding_button_receive_crypto) + btnMainAction.setOnClickListener { + store.dispatch(TwinCardsAction.ShowAddressInfoDialog) + } + + btnAlternativeAction.isVisible = false } - btn_alternative_action.isVisible = true - btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address) - btn_alternative_action.setOnClickListener { - store.dispatch(TwinCardsAction.ShowAddressInfoDialog) + tvHeader.setText(R.string.onboarding_top_up_header) + tvBody.setText(R.string.onboarding_top_up_body) + + btnRefreshBalanceWidget.changeState(state.walletBalance.state) + if (btnRefreshBalanceWidget.isShowing != true) { + btnRefreshBalanceWidget.mainView.setOnClickListener { + store.dispatch(TwinCardsAction.Balance.Update) + } } - } else { - btn_main_action.setText(R.string.onboarding_button_receive_crypto) - btn_main_action.setOnClickListener { - store.dispatch(TwinCardsAction.ShowAddressInfoDialog) + binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( + requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8) + ) + updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins) + } + + private fun setupDoneState(state: TwinCardsState) = + with(binding.onboardingActionContainer) { + btnMainAction.setText(R.string.onboarding_done_button_continue) + btnMainAction.setOnClickListener { + store.dispatch(TwinCardsAction.Confetti.Hide) + store.dispatch(TwinCardsAction.Done) } - btn_alternative_action.isVisible = false - } + btnAlternativeAction.isVisible = false + btnAlternativeAction.isClickable = false - tv_header.setText(R.string.onboarding_top_up_header) - tv_body.setText(R.string.onboarding_top_up_body) + tvHeader.setText(R.string.onboarding_done_header) + tvBody.setText(R.string.onboarding_done_body) - btnRefreshBalanceWidget.changeState(state.walletBalance.state) - if (btnRefreshBalanceWidget.isShowing != true) { - btnRefreshBalanceWidget.mainView.setOnClickListener { - store.dispatch(TwinCardsAction.Balance.Update) + val layout = when (state.mode) { + CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins + CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done } + updateConstraints(state.currentStep, layout) } - imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)) - updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins) - } - - private fun setupDoneState(state: TwinCardsState) { - btn_main_action.setText(R.string.onboarding_done_button_continue) - btn_main_action.setOnClickListener { - store.dispatch(TwinCardsAction.Confetti.Hide) - store.dispatch(TwinCardsAction.Done) - } - - btn_alternative_action.isVisible = false - btn_alternative_action.isClickable = false - - tv_header.setText(R.string.onboarding_done_header) - tv_body.setText(R.string.onboarding_done_body) - - val layout = when (state.mode) { - CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins - CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done - } - updateConstraints(state.currentStep, layout) - } private fun updateConstraints(currentStep: TwinCardsStep, @LayoutRes layoutId: Int) { if (this.previousStep == currentStep) return - val constraintSet = ConstraintSet() - constraintSet.clone(requireContext(), layoutId) - constraintSet.applyTo(onboarding_main_container) - val transition = InternalNoteLayoutTransition() - transition.interpolator = OvershootInterpolator() - TransitionManager.beginDelayedTransition(onboarding_main_container, transition) + with(binding.onboardingTopContainer) { + val constraintSet = ConstraintSet() + constraintSet.clone(requireContext(), layoutId) + constraintSet.applyTo(onboardingMainContainer) + val transition = InternalNoteLayoutTransition() + transition.interpolator = OvershootInterpolator() + TransitionManager.beginDelayedTransition(onboardingMainContainer, transition) + } } - private fun switchToCard(cardNumber: TwinCardNumber?, animate: Boolean = true, onEnd: VoidCallback = {}) { + private fun switchToCard( + cardNumber: TwinCardNumber?, + animate: Boolean = true, + onEnd: VoidCallback = {} + ) { val position = cardNumber?.number ?: return if (twinsWidget.leapfrogWidget.getViewPositionByIndex(1) != position - 1) { 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..9abbebd018 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 @@ -8,7 +8,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.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.withMainContext import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -16,6 +15,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 @@ -124,10 +124,15 @@ private fun handleWalletAction(action: Action) { } else { null } - BackupAction.IntroduceBackup(url) + if (walletState.backupState.backupStep == BackupStep.InitBackup || + walletState.backupState.backupStep == BackupStep.Finished) { + BackupAction.IntroduceBackup(url) + } else { + null + } } } - store.dispatch(newAction) + newAction?.let { store.dispatch(it) } } OnboardingWalletAction.OnBackPressed -> { when (walletState.backupState.backupStep) { @@ -164,14 +169,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 @@ -215,13 +222,11 @@ private fun handleBackupAction(action: BackupAction) { } } is BackupAction.GoToShop -> { - backupState.buyAdditionalCardsUrl?.let { url -> - store.dispatchOpenUrl(url) - store.state.globalState.analyticsHandlers?.triggerEvent( - event = AnalyticsEvent.GET_CARD, - params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.ONBOARDING.param) - ) - } + store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop)) + store.state.globalState.analyticsHandlers?.triggerEvent( + event = AnalyticsEvent.GET_CARD, + params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.ONBOARDING.param) + ) } is BackupAction.FinishAddingBackupCards -> { if (backupService.addedBackupCardsCount == 1) { @@ -233,9 +238,11 @@ private fun handleBackupAction(action: BackupAction) { } is BackupAction.CheckAccessCode -> { if (action.accessCode.length < 4) { - store.dispatch(BackupAction.SetAccessCodeError( - AccessCodeError.CodeTooShort - )) + store.dispatch( + BackupAction.SetAccessCodeError( + AccessCodeError.CodeTooShort + ) + ) } else { store.dispatch(BackupAction.SetAccessCodeError(null)) store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode)) @@ -247,9 +254,11 @@ private fun handleBackupAction(action: BackupAction) { backupService.setAccessCode(action.accessCodeConfirmation) store.dispatch(BackupAction.PrepareToWritePrimaryCard) } else { - store.dispatch(BackupAction.SetAccessCodeError( - AccessCodeError.CodesDoNotMatch - )) + store.dispatch( + BackupAction.SetAccessCodeError( + AccessCodeError.CodesDoNotMatch + ) + ) } } is BackupAction.WritePrimaryCard -> { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupInfoAdapter.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupInfoAdapter.kt index f7044f9986..3388e72a96 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupInfoAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupInfoAdapter.kt @@ -1,28 +1,30 @@ package com.tangem.tap.features.onboarding.products.wallet.ui import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import com.tangem.tap.common.extensions.getString import com.tangem.wallet.R -import kotlinx.android.synthetic.main.item_backup_info_adapter.view.* +import com.tangem.wallet.databinding.ItemBackupInfoAdapterBinding class BackupInfoAdapter : RecyclerView.Adapter() { private val data = backupInfoSnippets override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BackupInfoViewHolder = - BackupInfoViewHolder(LayoutInflater.from(parent.context) - .inflate(R.layout.item_backup_info_adapter, parent, false)) + BackupInfoViewHolder( + ItemBackupInfoAdapterBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) override fun getItemCount(): Int = data.size override fun onBindViewHolder(holder: BackupInfoViewHolder, position: Int) = - holder.itemView.run { - tv_header.text = getString(data[position].header) - tv_body.text = getString(data[position].body) + holder.binding.run { + tvHeader.text = root.getString(data[position].header) + tvBody.text = root.getString(data[position].body) } } @@ -51,4 +53,5 @@ private val backupInfoSnippets = listOf( ) -class BackupInfoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) \ No newline at end of file +class BackupInfoViewHolder(val binding: ItemBackupInfoAdapterBinding) : + RecyclerView.ViewHolder(binding.root) \ No newline at end of file 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..056eb28b12 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,18 +1,21 @@ 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 import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater import androidx.transition.TransitionManager +import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.tabs.TabLayoutMediator 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 @@ -21,18 +24,21 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.* import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_onboarding_wallet.* -import kotlinx.android.synthetic.main.layout_onboarding_buttons_add_cards.* -import kotlinx.android.synthetic.main.layout_onboarding_buttons_common.* -import kotlinx.android.synthetic.main.view_confetti.* -import kotlinx.android.synthetic.main.view_onboarding_progress.* +import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding +import com.tangem.wallet.databinding.ViewOnboardingProgressBinding import org.rekotlin.StoreSubscriber 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 + private val binding: FragmentOnboardingWalletBinding by viewBinding( + FragmentOnboardingWalletBinding::bind + ) + private val pbBinding: ViewOnboardingProgressBinding by viewBinding( + ViewOnboardingProgressBinding::bind) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -47,19 +53,29 @@ 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(binding.flCardsContainer, leapfrogCalculator) + cardsWidget = WalletCardsWidget(leapfrog, deviceScaleFactor) { 200f * deviceScaleFactor } startPostponedEnterTransition() - view_pager_backup_info.adapter = BackupInfoAdapter() - TabLayoutMediator(tab_layout_backup_info, view_pager_backup_info) { tab, position -> + binding.viewPagerBackupInfo.adapter = BackupInfoAdapter() + TabLayoutMediator( + binding.tabLayoutBackupInfo, + binding.viewPagerBackupInfo + ) { tab, position -> //Some implementation }.attach() - (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) store.dispatch(OnboardingWalletAction.Init) - toolbar.setNavigationOnClickListener { activity?.onBackPressed() } + binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() } store.dispatch(OnboardingWalletAction.LoadArtwork) @@ -81,16 +97,18 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), } override fun newState(state: OnboardingWalletState) { - if (activity == null) return + if (activity == null || view == null) return requireActivity().invalidateOptionsMenu() - loadImageIntoImageView(state.cardArtworkUrl, imv_front_card) - loadImageIntoImageView(state.cardArtworkUrl, imv_first_backup_card) - loadImageIntoImageView(state.cardArtworkUrl, imv_second_backup_card) + with(binding) { + loadImageIntoImageView(state.cardArtworkUrl, imvFrontCard) + loadImageIntoImageView(state.cardArtworkUrl, imvFirstBackupCard) + loadImageIntoImageView(state.cardArtworkUrl, imvSecondBackupCard) + } - pb_state.max = 6 - pb_state.progress = state.getProgressStep() + pbBinding.pbState.max = 6 + pbBinding.pbState.progress = state.getProgressStep() when (state.step) { OnboardingWalletStep.None -> { @@ -111,25 +129,24 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), ?.into(view) } - private fun setupCreateWalletState() { - btn_main_action.setText(R.string.onboarding_create_wallet_button_create_wallet) - btn_main_action.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) } - btn_alternative_action.hide() + private fun setupCreateWalletState() = with(binding) { + layoutButtonsCommon.btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) + layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) } + layoutButtonsCommon.btnAlternativeAction.hide() toolbar.title = getText(R.string.onboarding_getting_started) - tv_header.setText(R.string.onboarding_create_wallet_header) - tv_body.setText(R.string.onboarding_create_wallet_body) + tvHeader.setText(R.string.onboarding_create_wallet_header) + tvBody.setText(R.string.onboarding_create_wallet_body) - cardsWidget.toFolded() - startPostponedEnterTransition() + cardsWidget.toFolded(false) { startPostponedEnterTransition() } } private fun setBackupState(state: BackupState) { when (state.backupStep) { BackupStep.InitBackup -> showBackupIntro(state) - BackupStep.ScanOriginCard -> showScanOriginCard(state) + BackupStep.ScanOriginCard -> showScanOriginCard() BackupStep.AddBackupCards -> showAddBackupCards(state) BackupStep.SetAccessCode -> showSetAccessCode() BackupStep.EnterAccessCode -> showEnterAccessCode(state) @@ -140,65 +157,70 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), } } - private fun showBackupIntro(state: BackupState) { - imv_first_backup_card.show() - imv_second_backup_card.show() + private fun showBackupIntro(state: BackupState) = with(binding) { + imvFirstBackupCard.show() + imvSecondBackupCard.show() cardsWidget.toWelcome() - tv_header.hide() - tv_body.hide() - view_pager_backup_info.show() - tab_layout_backup_info.show() + tvHeader.hide() + tvBody.hide() + viewPagerBackupInfo.show() + tabLayoutBackupInfo.show() + + with(layoutButtonsCommon) { + btnMainAction.text = getText(R.string.onboarding_button_backup_now) + btnAlternativeAction.text = getText(R.string.onboarding_button_skip_backup) + btnAlternativeAction.show(state.canSkipBackup) + btnMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) } + btnAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) } + } - btn_main_action.text = getText(R.string.onboarding_button_backup_now) - btn_alternative_action.text = getText(R.string.onboarding_button_skip_backup) - btn_alternative_action.show(state.canSkipBackup) - btn_main_action.setOnClickListener { store.dispatch(BackupAction.StartBackup) } - btn_alternative_action.setOnClickListener { store.dispatch(BackupAction.DismissBackup) } startPostponedEnterTransition() } - private fun showScanOriginCard(state: BackupState) { + private fun showScanOriginCard() = with(binding) { prepareBackupView() cardsWidget.toFolded() - tv_header.text = getText(R.string.onboarding_title_scan_origin_card) - tv_body.text = getString( + tvHeader.text = getText(R.string.onboarding_title_scan_origin_card) + tvBody.text = getString( R.string.onboarding_subtitle_scan_origin_card, ) - btn_main_action.text = getString(R.string.onboarding_button_scan_origin_card) - btn_alternative_action.hide() - btn_main_action.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) } + with(layoutButtonsCommon) { + btnMainAction.text = getString(R.string.onboarding_button_scan_origin_card) + btnAlternativeAction.hide() + btnMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) } + } } - private fun prepareBackupView() { + private fun prepareBackupView() = with(binding) { toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup) - tv_header.show() - tv_body.show() - view_pager_backup_info.hide() - tab_layout_backup_info.hide() + tvHeader.show() + tvBody.show() + viewPagerBackupInfo.hide() + tabLayoutBackupInfo.hide() - imv_first_backup_card.show() - imv_second_backup_card.show() + imvFirstBackupCard.show() + imvSecondBackupCard.show() } - private fun showAddBackupCards(state: BackupState) { + private fun showAddBackupCards(state: BackupState) = with(binding) { prepareBackupView() accessCodeDialog?.dismiss() accessCodeDialog = null - layout_buttons_add_cards.show() - layout_buttons_common.hide() - btn_add_card.text = getText(R.string.onboarding_button_add_backup_card) + layoutButtonsAddCards.root.show() + layoutButtonsCommon.root.hide() + layoutButtonsAddCards.btnAddCard.text = getText(R.string.onboarding_button_add_backup_card) if (state.backupCardsNumber < state.maxBackupCards) { - btn_add_card.setOnClickListener { store.dispatch(BackupAction.AddBackupCard) } + layoutButtonsAddCards.btnAddCard.setOnClickListener { store.dispatch(BackupAction.AddBackupCard) } } else { - btn_add_card.isEnabled = false + layoutButtonsAddCards.btnAddCard.isEnabled = false } when (state.backupCardsNumber) { @@ -207,27 +229,30 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), cardsWidget.getFirstBackupCardView().animate().alpha(0.6f).setDuration(200) cardsWidget.getSecondBackupCardView().animate().alpha(0.2f).setDuration(200) } - tv_header.text = getText(R.string.onboarding_title_no_backup_cards) - tv_body.text = getText(R.string.onboarding_subtitle_no_backup_cards) + tvHeader.text = getText(R.string.onboarding_title_no_backup_cards) + tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards) } 1 -> { - tv_header.text = getText(R.string.onboarding_title_one_backup_card) - tv_body.text = getText(R.string.onboarding_subtitle_one_backup_card) + tvHeader.text = getText(R.string.onboarding_title_one_backup_card) + tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card) + cardsWidget.toFan(false) cardsWidget.getFirstBackupCardView().animate().alpha(1f).setDuration(400) cardsWidget.getSecondBackupCardView().alpha = 0.2f } 2 -> { - tv_header.text = getText(R.string.onboarding_title_two_backup_cards) - tv_body.text = getText(R.string.onboarding_subtitle_two_backup_cards) + tvHeader.text = getText(R.string.onboarding_title_two_backup_cards) + tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards) + + cardsWidget.toFan(false) cardsWidget.getFirstBackupCardView().alpha = 1f cardsWidget.getSecondBackupCardView().animate().alpha(1f).setDuration(400) } } - btn_continue.text = getText(R.string.onboarding_button_finalize_backup) - btn_continue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) } - btn_continue.isEnabled = state.backupCardsNumber != 0 + layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup) + layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) } + layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0 } private fun showSetAccessCode() { @@ -258,7 +283,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), accessCodeDialog?.showError(state.accessCodeError) } - private fun showWritePrimaryCard(state: BackupState) { + private fun showWritePrimaryCard(state: BackupState) = with(binding) { accessCodeDialog?.dismiss() prepareViewForFinalizeStep() @@ -270,33 +295,33 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), } val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) - tv_header.text = getText(R.string.onboarding_title_prepare_origin) - tv_body.text = getString( + tvHeader.text = getText(R.string.onboarding_title_prepare_origin) + tvBody.text = getString( R.string.onboarding_subtitle_scan_primary_card_format, state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) } ) - btn_main_action.text = getText(R.string.onboarding_button_backup_origin) - btn_main_action.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) } + layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_backup_origin) + layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) } } - private fun prepareViewForFinalizeStep() { - layout_buttons_add_cards.hide() - layout_buttons_common.show() + private fun prepareViewForFinalizeStep() = with(binding) { + layoutButtonsAddCards.root.hide() + layoutButtonsCommon.root.show() - imv_first_backup_card.show() - imv_second_backup_card.show() + imvFirstBackupCard.show() + imvSecondBackupCard.show() toolbar.title = getText(R.string.onboarding_button_finalize_backup) - imv_card_background.hide() + imvCardBackground.hide() cardsWidget.toLeapfrog() - btn_alternative_action.hide() + layoutButtonsCommon.btnAlternativeAction.hide() } - private fun showWriteBackupCard(state: BackupState) { + private fun showWriteBackupCard(state: BackupState) = with(binding) { prepareViewForFinalizeStep() cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2) @@ -316,46 +341,47 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), } val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) - tv_header.text = + tvHeader.text = getString(R.string.onboarding_title_backup_card_format, cardNumber) - tv_body.text = getString( + tvBody.text = getString( R.string.onboarding_subtitle_scan_backup_card_format, cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]) ) - btn_main_action.text = getString( + layoutButtonsCommon.btnMainAction.text = getString( R.string.onboarding_button_backup_card_format, cardNumber ) - - btn_main_action.setOnClickListener { store.dispatch(BackupAction.WriteBackupCard(cardNumber)) } + layoutButtonsCommon.btnMainAction.setOnClickListener { + store.dispatch(BackupAction.WriteBackupCard(cardNumber)) + } } - private fun showSuccess() { - tv_header.text = getText(R.string.onboarding_done_header) + private fun showSuccess() = with(binding) { + tvHeader.text = getText(R.string.onboarding_done_header) - tv_header.show() - tv_body.show() - view_pager_backup_info.hide() - tab_layout_backup_info.hide() + tvHeader.show() + tvBody.show() + viewPagerBackupInfo.hide() + tabLayoutBackupInfo.hide() - tv_body.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding) - btn_main_action.text = getText(R.string.onboarding_button_continue_wallet) - btn_alternative_action.hide() - btn_main_action.setOnClickListener { - lav_confetti.cancelAnimation() - lav_confetti.hide() + tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding) + layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_continue_wallet) + layoutButtonsCommon.btnAlternativeAction.hide() + layoutButtonsCommon.btnMainAction.setOnClickListener { + vConfetti.lavConfetti.cancelAnimation() + vConfetti.lavConfetti.hide() store.dispatch(OnboardingWalletAction.FinishOnboarding) } cardsWidget.leapfrogWidget.fold { - fl_cards_container?.hide() - imv_card_background?.hide() - lav_confetti?.show() - lav_confetti?.playAnimation() - imv_success?.alpha = 0f - imv_success?.show() + flCardsContainer.hide() + imvCardBackground.hide() + vConfetti.lavConfetti.show() + vConfetti.lavConfetti.playAnimation() + imvSuccess.alpha = 0f + imvSuccess.show() - imv_success?.animate() + imvSuccess.animate() ?.alpha(1f) ?.setDuration(400) } @@ -379,7 +405,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..9f42b3be0b 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 = {}) { @@ -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/onboarding/products/wallet/ui/dialogs/AccessCodeDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AccessCodeDialog.kt index 9433d4e274..c8b5e3ccb0 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AccessCodeDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AccessCodeDialog.kt @@ -1,8 +1,9 @@ package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs import android.content.Context +import android.content.DialogInterface +import android.os.Bundle import android.view.LayoutInflater -import android.view.View import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.tangem.tap.common.extensions.hide @@ -11,64 +12,69 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.AccessCodeError import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.layout_backup_access_code.* -import kotlinx.android.synthetic.main.layout_backup_access_code_info.* -import kotlinx.android.synthetic.main.layout_backup_access_code_submit.* +import com.tangem.wallet.databinding.LayoutBackupAccessCodeBinding class AccessCodeDialog(context: Context) : BottomSheetDialog(context) { + var binding: LayoutBackupAccessCodeBinding? = null - init { - behavior.state = BottomSheetBehavior.STATE_EXPANDED - val dialogView = LayoutInflater.from(context).inflate(R.layout.layout_backup_access_code, null) - setContentView(dialogView) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = LayoutBackupAccessCodeBinding + .inflate(LayoutInflater.from(context)) + setContentView(binding!!.root) behavior.state = BottomSheetBehavior.STATE_EXPANDED } - override fun setContentView(view: View) { - super.setContentView(view) - behavior.state = BottomSheetBehavior.STATE_EXPANDED + override fun setOnDismissListener(listener: DialogInterface.OnDismissListener?) { + super.setOnDismissListener(listener) + binding = null } - - fun showInfoScreen() { + fun showInfoScreen() = with(binding!!) { behavior.state = BottomSheetBehavior.STATE_EXPANDED - access_code_title.text = context.getText(R.string.onboarding_access_code_intro_title) - layout_backup_access_code_info.show() - layout_backup_access_code_submit.hide() - btn_access_code_create.setOnClickListener { + accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title) + layoutBackupAccessCodeInfo.root.show() + layoutBackupAccessCodeSubmit.root.hide() + layoutBackupAccessCodeInfo.btnAccessCodeCreate.setOnClickListener { store.dispatch(BackupAction.ShowEnterAccessCodeScreen) } } - fun showEnterAccessCode() { - layout_backup_access_code_info.hide() - layout_backup_access_code_submit.show() - access_code_title.text = context.getText(R.string.onboarding_access_code_intro_title) - btn_access_code_submit.text = context.getText(R.string.common_continue) - btn_access_code_submit.setOnClickListener { - store.dispatch(BackupAction.CheckAccessCode(et_access_code.text.toString())) + fun showEnterAccessCode() = with(binding!!) { + layoutBackupAccessCodeInfo.root.hide() + layoutBackupAccessCodeSubmit.root.show() + accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title) + + with(layoutBackupAccessCodeSubmit) { + btnAccessCodeSubmit.text = context.getText(R.string.common_continue) + btnAccessCodeSubmit.setOnClickListener { + store.dispatch(BackupAction.CheckAccessCode(etAccessCode.text.toString())) + } } } - fun showReenterAccessCode() { - layout_backup_access_code_info.hide() - layout_backup_access_code_submit.show() - access_code_title.text = context.getText(R.string.onboarding_access_code_repeat_code_title) - et_access_code.setText("") - btn_access_code_submit.text = context.getText(R.string.common_submit) - btn_access_code_submit.setOnClickListener { - store.dispatch(BackupAction.SaveAccessCodeConfirmation(et_access_code.text.toString())) + fun showReenterAccessCode() = with(binding!!) { + layoutBackupAccessCodeInfo.root.hide() + layoutBackupAccessCodeSubmit.root.show() + accessCodeTitle.text = context.getText(R.string.onboarding_access_code_repeat_code_title) + with(layoutBackupAccessCodeSubmit) { + etAccessCode.setText("") + btnAccessCodeSubmit.text = context.getText(R.string.common_submit) + btnAccessCodeSubmit.setOnClickListener { + store.dispatch(BackupAction.SaveAccessCodeConfirmation(etAccessCode.text.toString())) + } } + } - fun showError(error: AccessCodeError?) { + fun showError(error: AccessCodeError?) = with(binding!!.layoutBackupAccessCodeSubmit) { when (error) { AccessCodeError.CodeTooShort -> - til_access_code.error = context.getString(R.string.onboarding_access_code_too_short) + tilAccessCode.error = context.getString(R.string.onboarding_access_code_too_short) AccessCodeError.CodesDoNotMatch -> - til_access_code.error = context.getString(R.string.onboarding_access_codes_doesnt_match) - null -> til_access_code.error = null + tilAccessCode.error = context.getString(R.string.onboarding_access_codes_doesnt_match) + null -> tilAccessCode.error = null } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt index 04d5b420d5..29165f8bad 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -6,6 +6,8 @@ import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.demo.DemoTransactionSender +import com.tangem.tap.features.demo.isDemoWallet import com.tangem.tap.features.send.redux.AmountActionUi import com.tangem.tap.features.send.redux.FeeAction import com.tangem.tap.features.send.redux.ReceiptAction @@ -38,7 +40,11 @@ class RequestFeeMiddleware { val destinationAddress = sendState.addressPayIdState.destinationWalletAddress!! val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto) - val txSender = walletManager as TransactionSender + val txSender = if (walletManager.isDemoWallet()) { + DemoTransactionSender(walletManager) + } else { + walletManager as TransactionSender + } scope.launch { val feeResult = txSender.getFee(destinationAmount, destinationAddress) withContext(Dispatchers.Main) { @@ -84,15 +90,15 @@ class FeeMock { suspend fun feeZero(blockchain: Blockchain): List = listOf(Amount(BigDecimal.ZERO, blockchain)) suspend fun feeStandard(blockchain: Blockchain): List = listOf( - Amount(0.001500.toBigDecimal(), blockchain), - Amount(0.0030.toBigDecimal(), blockchain), - Amount(0.0045001.toBigDecimal(), blockchain) + Amount(0.001500.toBigDecimal(), blockchain), + Amount(0.0030.toBigDecimal(), blockchain), + Amount(0.0045001.toBigDecimal(), blockchain) ) suspend fun feeStandardBig(blockchain: Blockchain): List = listOf( - Amount(1.76.toBigDecimal(), blockchain), - Amount(2.30.toBigDecimal(), blockchain), - Amount(3.45.toBigDecimal(), blockchain) + Amount(1.76.toBigDecimal(), blockchain), + Amount(2.30.toBigDecimal(), blockchain), + Amount(3.45.toBigDecimal(), blockchain) ) suspend fun feeSingle(blockchain: Blockchain): List = listOf(Amount(0.0015.toBigDecimal(), blockchain)) 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..f103fc3817 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,13 @@ 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) -> { + delay(DELAY_SDK_DIALOG_CLOSE) + store.dispatchDialogShow(AppDialog.SimpleOkDialogRes( + R.string.common_done, + R.string.alert_demo_feature_disabled + ) { dispatch(NavigationAction.PopBackTo()) }) + } else -> { (sendResult.error as? TangemSdkError)?.let { error -> store.state.globalState.analyticsHandlers?.logCardSdkError( diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index eab62a8479..d999d79bcb 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -11,6 +11,7 @@ import androidx.core.view.postDelayed import androidx.core.widget.addTextChangedListener import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.textfield.TextInputEditText import com.tangem.Message import com.tangem.tangem_sdk_new.extensions.dpToPx @@ -40,10 +41,7 @@ import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.mainScope import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_send.* -import kotlinx.android.synthetic.main.layout_send_address_payid.* -import kotlinx.android.synthetic.main.layout_send_amount.* -import kotlinx.android.synthetic.main.layout_send_fee.* +import com.tangem.wallet.databinding.FragmentSendBinding import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* @@ -62,6 +60,8 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { private val sendSubscriber = SendStateSubscriber(this) private lateinit var keyboardObserver: KeyboardObserver + val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) addBackPressHandler(this) @@ -77,7 +77,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { store.dispatch(SendActionUi.CheckIfTransactionDataWasProvided) } - private fun initSendButtonStates() { + private fun initSendButtonStates() = with(binding) { btnSend.setOnClickListener { store.dispatch(SendActionUi.SendAmountToRecipient( Message(getString(R.string.initial_message_sign_header)) @@ -86,7 +86,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { sendBtn = IndeterminateProgressButtonWidget(btnSend, progress) } - private fun setupAddressOrPayIdLayout() { + private fun setupAddressOrPayIdLayout() = with(binding.lSendAddressPayid) { store.dispatch(SetTruncateHandler { etAddressOrPayId.truncateMiddleWith(it, "...") }) store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString())) @@ -113,7 +113,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { } } - private fun setupTransactionExtrasLayout() { + private fun setupTransactionExtrasLayout() = with(binding.lSendAddressPayid) { etXlmMemo.inputtedTextAsFlow() .debounce(400) .filter { @@ -151,9 +151,9 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { // Delayed launch is needed in order for the UI to be drawn and to process the sent events. // If do not use the delay, then etAmount error field is not displayed when // inserting an incorrect amount by shareUri - imvQrCode.postDelayed({ + binding.lSendAddressPayid.imvQrCode.postDelayed({ store.dispatch(PasteAddressPayId(scannedCode)) - store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused)) + store.dispatch(TruncateOrRestore(!binding.lSendAddressPayid.etAddressOrPayId.isFocused)) }, 200) } @@ -165,7 +165,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator.toString() store.dispatch(AmountAction.SetDecimalSeparator(decimalSeparator)) - tvAmountCurrency.setOnClickListener { + binding.lSendAmount.tvAmountCurrency.setOnClickListener { store.dispatch(ToggleMainCurrency) store.dispatch(ReceiptAction.RefreshReceipt) store.dispatch(SendAction.ChangeSendButtonState(store.state.sendState.getButtonState())) @@ -223,8 +223,8 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { } } - private fun setupFeeLayout() { - flExpandCollapse.setOnClickListener { + private fun setupFeeLayout() = with(binding.clNetworkFee) { + flExpandCollapse.flExpandCollapse.setOnClickListener { store.dispatch(ToggleControlsVisibility) } chipGroup.check(FeeUiHelper.toId(FeeType.NORMAL)) @@ -240,12 +240,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { } } - private fun setupWarningMessages() { + private fun setupWarningMessages() = with(binding) { warningsAdapter = WarningMessagesAdapter() val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - rv_warning_messages.layoutManager = layoutManager - rv_warning_messages.addItemDecoration(SpacesItemDecoration(rv_warning_messages.dpToPx(16f).toInt())) - rv_warning_messages.adapter = warningsAdapter + rvWarningMessages.layoutManager = layoutManager + rvWarningMessages.addItemDecoration(SpacesItemDecoration(rvWarningMessages.dpToPx(16f).toInt())) + rvWarningMessages.adapter = warningsAdapter store.dispatch(SendAction.Warnings.Update) } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 894d9809ba..3c49da49fb 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -26,32 +26,30 @@ import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.btn_expand_collapse.* -import kotlinx.android.synthetic.main.fragment_send.* -import kotlinx.android.synthetic.main.fragment_send.clReceiptContainer -import kotlinx.android.synthetic.main.layout_receipt_total.* -import kotlinx.android.synthetic.main.layout_receipt_total.view.* -import kotlinx.android.synthetic.main.layout_send_address_payid.* -import kotlinx.android.synthetic.main.layout_send_amount.* -import kotlinx.android.synthetic.main.layout_send_fee.* -import kotlinx.android.synthetic.main.layout_send_receipt.* /** [REDACTED_AUTHOR] */ -class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber(fragment) { +class SendStateSubscriber(fragment: BaseStoreFragment) : + FragmentStateSubscriber(fragment) { private var dialog: Dialog? = null override fun updateWithNewState(fg: BaseStoreFragment, state: SendState) { + fg.view ?: return + if (fg !is SendFragment) return + val lastChangedStates = state.lastChangedStates.toList() state.lastChangedStates.clear() - fg.main_send_container.beginDelayedTransition() + (fg as? SendFragment)?.binding?.mainSendContainer?.beginDelayedTransition() lastChangedStates.forEach { when (it) { StateId.SEND_SCREEN -> handleSendScreen(fg, state) StateId.ADDRESS_PAY_ID -> handleAddressPayIdState(fg, state.addressPayIdState) - StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState(fg, state.transactionExtrasState) + StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState( + fg, + state.transactionExtrasState + ) StateId.AMOUNT -> handleAmountState(fg, state.amountState) StateId.FEE -> handleFeeState(fg, state.feeState) StateId.RECEIPT -> handleReceiptState(fg, state.receiptState) @@ -59,57 +57,58 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber } } - private fun handleTransactionExtrasState(fg: BaseStoreFragment, infoState: TransactionExtrasState) { - fun showView(view: View, info: Any?) { - view.show(info != null) - } - showView(fg.xlmMemoContainer, infoState.xlmMemo) - showView(fg.xrpDestinationTagContainer, infoState.xrpDestinationTag) - showView(fg.binanceMemoContainer, infoState.binanceMemo) + private fun handleTransactionExtrasState(fg: SendFragment, infoState: TransactionExtrasState) = + with(fg.binding.lSendAddressPayid) { - infoState.xlmMemo?.let { - fg.etXlmMemo.inputType = when (it.selectedMemoType) { - XlmMemoType.TEXT -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS - XlmMemoType.ID -> InputType.TYPE_CLASS_NUMBER + fun showView(view: View, info: Any?) { + view.show(info != null) } - if (!it.viewFieldValue.isFromUserInput) fg.etXlmMemo.setText(it.viewFieldValue.value) - if (it.error != null) { - if (it.error == TransactionExtraError.INVALID_XLM_MEMO) { - fg.tilXlmMemo.error = fg.getText(R.string.send_error_invalid_memo_id) - } - } else { - fg.tilXlmMemo.error = null - } - } - infoState.xrpDestinationTag?.let { - if (infoState.xrpDestinationTag.error != null) { - if (infoState.xrpDestinationTag.error == TransactionExtraError.INVALID_DESTINATION_TAG) { - fg.tilDestinationTag.error = fg.getText(R.string.send_error_invalid_destination_tag) - } - } else { - fg.tilDestinationTag.error = null - } - if (!it.viewFieldValue.isFromUserInput) { - fg.etDestinationTag.setText(it.viewFieldValue.value) - } - } - infoState.binanceMemo?.let { - if (infoState.binanceMemo.error != null) { - if (infoState.binanceMemo.error == TransactionExtraError.INVALID_BINANCE_MEMO) { - fg.tilBinanceMemo.error = fg.getText(R.string.send_error_invalid_memo_id) - } - } else { - fg.tilBinanceMemo.error = null - } - if (!it.viewFieldValue.isFromUserInput) { - fg.etBinanceMemo.setText(it.viewFieldValue.value) - } - } - } + showView(xlmMemoContainer, infoState.xlmMemo) + showView(xrpDestinationTagContainer, infoState.xrpDestinationTag) + showView(binanceMemoContainer, infoState.binanceMemo) - private fun handleSendScreen(fg: BaseStoreFragment, state: SendState) { - val sendFragment = (fg as? SendFragment) ?: return + infoState.xlmMemo?.let { + etXlmMemo.inputType = when (it.selectedMemoType) { + XlmMemoType.TEXT -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + XlmMemoType.ID -> InputType.TYPE_CLASS_NUMBER + } + if (!it.viewFieldValue.isFromUserInput) etXlmMemo.setText(it.viewFieldValue.value) + if (it.error != null) { + if (it.error == TransactionExtraError.INVALID_XLM_MEMO) { + tilXlmMemo.error = fg.getText(R.string.send_error_invalid_memo_id) + } + } else { + tilXlmMemo.error = null + } + } + infoState.xrpDestinationTag?.let { + if (infoState.xrpDestinationTag.error != null) { + if (infoState.xrpDestinationTag.error == TransactionExtraError.INVALID_DESTINATION_TAG) { + tilDestinationTag.error = + fg.getText(R.string.send_error_invalid_destination_tag) + } + } else { + tilDestinationTag.error = null + } + if (!it.viewFieldValue.isFromUserInput) { + etDestinationTag.setText(it.viewFieldValue.value) + } + } + infoState.binanceMemo?.let { + if (infoState.binanceMemo.error != null) { + if (infoState.binanceMemo.error == TransactionExtraError.INVALID_BINANCE_MEMO) { + tilBinanceMemo.error = fg.getText(R.string.send_error_invalid_memo_id) + } + } else { + tilBinanceMemo.error = null + } + if (!it.viewFieldValue.isFromUserInput) { + etBinanceMemo.setText(it.viewFieldValue.value) + } + } + } + private fun handleSendScreen(fg: SendFragment, state: SendState) = with(fg.binding) { when (state.dialog) { is SendAction.Dialog.TezosWarningDialog -> { if (dialog == null) { @@ -129,147 +128,158 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber } } - sendFragment.sendBtn.changeState(state.sendButtonState.progressState) - sendFragment.sendBtn.mainView.isEnabled = state.sendButtonState.enabled + fg.sendBtn.changeState(state.sendButtonState.progressState) + fg.sendBtn.mainView.isEnabled = state.sendButtonState.enabled - val rv = fg.rv_warning_messages + val rv = rvWarningMessages val adapter = rv.adapter as? WarningMessagesAdapter ?: return adapter.submitList(state.sendWarningsList) rv.show(state.sendWarningsList.isNotEmpty()) - fg.toolbar.title = fg.getString( + toolbar.title = fg.getString( R.string.send_title_currency_format, state.amountState.mainCurrency.currencySymbol ) } - private fun handleAddressPayIdState(fg: BaseStoreFragment, state: AddressPayIdState) { - fun parseError(context: Context, error: Error?): String? { - val resId = when (error) { - Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_error_payid_unsupported_by_blockchain - Error.PAY_ID_NOT_REGISTERED -> R.string.send_error_payid_not_registered - Error.PAY_ID_REQUEST_FAILED -> R.string.send_error_payid_request_failed - Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_validation_invalid_address - Error.ADDRESS_SAME_AS_WALLET -> R.string.send_error_address_same_as_wallet - else -> null + private fun handleAddressPayIdState(fg: SendFragment, state: AddressPayIdState) = + with(fg.binding.lSendAddressPayid) { + fun parseError(context: Context, error: Error?): String? { + val resId = when (error) { + Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_error_payid_unsupported_by_blockchain + Error.PAY_ID_NOT_REGISTERED -> R.string.send_error_payid_not_registered + Error.PAY_ID_REQUEST_FAILED -> R.string.send_error_payid_request_failed + Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_validation_invalid_address + Error.ADDRESS_SAME_AS_WALLET -> R.string.send_error_address_same_as_wallet + else -> null + } + return if (resId == null) null else context.getString(resId, "", "") } - return if (resId == null) null else context.getString(resId, "", "") + + imvPaste.isEnabled = state.pasteIsEnabled + + val et = etAddressOrPayId + val til = tilAddressOrPayId + val parsedError = parseError(til.context, state.error) + + til.isEnabled = state.inputIsEnabled + imvPaste.show(state.inputIsEnabled) + imvQrCode.show(state.inputIsEnabled) + flPaste.show(state.inputIsEnabled) + flQrCode.show(state.inputIsEnabled) + + val hintResId = if (state.sendingToPayIdEnabled) { + R.string.send_destination_hint_address_payid + } else { + R.string.send_destination_hint_address + } + til.hint = til.getString(hintResId) + til.error = parsedError + til.isErrorEnabled = parsedError != null + til.helperText = state.destinationWalletAddress + til.isHelperTextEnabled = state.isPayIdState() && parsedError == null + + if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value) } - fg.imvPaste.isEnabled = state.pasteIsEnabled - val et = fg.etAddressOrPayId - val til = fg.tilAddressOrPayId - val parsedError = parseError(til.context, state.error) - - til.isEnabled = state.inputIsEnabled - fg.imvPaste.show(state.inputIsEnabled) - fg.imvQrCode.show(state.inputIsEnabled) - fg.flPaste.show(state.inputIsEnabled) - fg.flQrCode.show(state.inputIsEnabled) - - val hintResId = if (state.sendingToPayIdEnabled) { - R.string.send_destination_hint_address_payid - } else { - R.string.send_destination_hint_address - } - til.hint = til.getString(hintResId) - til.error = parsedError - til.isErrorEnabled = parsedError != null - til.helperText = state.destinationWalletAddress - til.isHelperTextEnabled = state.isPayIdState() && parsedError == null - - if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value) - } - - private fun handleAmountState(fg: BaseStoreFragment, state: AmountState) { + private fun handleAmountState(fg: SendFragment, state: AmountState) = with(fg.binding.lSendAmount) { if (state.error != null) { val context = fg.requireContext() val message = when (state.error) { is MultiMessageError -> { val multiError = state.error as MultiMessageError - val messageList = multiError.assembleErrors().map { getMessageString(context, it.first, it.second) } + val messageList = multiError.assembleErrors() + .map { getMessageString(context, it.first, it.second) } multiError.builder(messageList) } else -> context.getString(state.error.messageResource) } - fg.tilAmountToSend.enableError(true, message) + tilAmountToSend.enableError(true, message) } else { - fg.tilAmountToSend.enableError(false) + tilAmountToSend.enableError(false) } val filter = DecimalDigitsInputFilter(12, state.maxLengthOfAmount, state.decimalSeparator) - fg.etAmountToSend.filters = arrayOf(filter) + etAmountToSend.filters = arrayOf(filter) val amountToSend = state.viewAmountValue - if (!amountToSend.isFromUserInput) fg.etAmountToSend.update(amountToSend.value) + if (!amountToSend.isFromUserInput) etAmountToSend.update(amountToSend.value) - fg.tvAmountCurrency.update(state.mainCurrency.currencySymbol) + tvAmountCurrency.update(state.mainCurrency.currencySymbol) (fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.type) val balanceText = when (state.mainCurrency.type) { - MainCurrencyType.FIAT -> fg.getString(R.string.send_balance_subtitle_format, - state.viewBalanceValue, state.mainCurrency.currencySymbol).remove(":") - MainCurrencyType.CRYPTO -> fg.getString(R.string.send_balance_subtitle_format, - state.mainCurrency.currencySymbol, state.viewBalanceValue) + MainCurrencyType.FIAT -> fg.getString( + R.string.send_balance_subtitle_format, + state.viewBalanceValue, state.mainCurrency.currencySymbol + ).remove(":") + MainCurrencyType.CRYPTO -> fg.getString( + R.string.send_balance_subtitle_format, + state.mainCurrency.currencySymbol, state.viewBalanceValue + ) } - fg.tvBalance.update(balanceText) + tvBalance.update(balanceText) - fg.tilAmountToSend.isEnabled = state.inputIsEnabled + tilAmountToSend.isEnabled = state.inputIsEnabled val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0 - fg.tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0) + tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0) val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray - fg.tvAmountCurrency.setTextColor(fg.getColor(textColor)) - + tvAmountCurrency.setTextColor(fg.getColor(textColor)) } - private fun handleFeeState(fg: BaseStoreFragment, state: FeeState) { - fg.chipGroup.fitChipsByGroupWidth() + private fun handleFeeState(fg: SendFragment, state: FeeState) = with(fg.binding.clNetworkFee) { +// chipGroup.fitChipsByGroupWidth() fg.view?.findViewById(R.id.clNetworkFee)?.show(state.mainLayoutIsVisible) + flExpandCollapse.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f + llFeeControlsContainer.show(state.controlsLayoutIsVisible) + chipGroup.show(state.feeChipGroupIsVisible) - fg.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f - fg.llFeeControlsContainer.show(state.controlsLayoutIsVisible) - fg.chipGroup.show(state.feeChipGroupIsVisible) - - fg.swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled - if (fg.swIncludeFee.isChecked != state.feeIsIncluded) { - fg.swIncludeFee.isChecked = state.feeIsIncluded + swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled + if (swIncludeFee.isChecked != state.feeIsIncluded) { + swIncludeFee.isChecked = state.feeIsIncluded } if (state.error == FeeAction.Error.REQUEST_FAILED) { - fg.showRetrySnackbar(fg.requireContext().getString(R.string.send_error_fee_request_failed)) { + fg.showRetrySnackbar( + fg.requireContext().getString(R.string.send_error_fee_request_failed) + ) { store.dispatch(FeeAction.RequestFee) } } val chipId = FeeUiHelper.toId(state.selectedFeeType) - if (fg.chipGroup.checkedChipId != chipId && chipId != View.NO_ID) fg.chipGroup.check(chipId) + if (chipGroup.checkedChipId != chipId && chipId != View.NO_ID) chipGroup.check(chipId) } - private fun handleReceiptState(fg: BaseStoreFragment, state: ReceiptState) { - val mainLayout = fg.clReceiptContainer as ViewGroup - val totalLayout = fg.llTotal as ViewGroup - val totalTokenLayout = fg.flTotalTokenCrypto as ViewGroup + private fun handleReceiptState(fg: SendFragment, state: ReceiptState) = with(fg.binding.clReceiptContainer) { + val mainLayout = clReceiptContainer as ViewGroup + val totalLayout = llTotalContainer.llTotal as ViewGroup + val totalTokenLayout = llTotalContainer.flTotalTokenCrypto as ViewGroup fun getString(id: Int, vararg formatStrings: String): String = - mainLayout.context.getString(id, *formatStrings) + mainLayout.context.getString(id, *formatStrings) val rough = getString(R.string.sign_rough) - fun roughOrEmpty(value: String): String = if (value == ReceiptReducer.EMPTY) value else "$rough $value" + fun roughOrEmpty(value: String): String = + if (value == ReceiptReducer.EMPTY) value else "$rough $value" when (state.visibleTypeOfReceipt) { ReceiptLayoutType.FIAT -> { val receipt = state.fiat ?: return + llTotalContainer.tvTotalValue totalLayout.show(true) totalTokenLayout.show(false) - fg.tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}") - fg.tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}") - totalLayout.tvTotalValue.update("${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}") + tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}") + tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}") + llTotalContainer.tvTotalValue.update("${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}") - val willSent = getString(R.string.send_total_subtitle_format, - receipt.willSentCrypto, receipt.symbols.crypto) - totalLayout.tvWillBeSentValue.update(willSent) + val willSent = getString( + R.string.send_total_subtitle_format, + receipt.willSentCrypto, receipt.symbols.crypto + ) + llTotalContainer.tvWillBeSentValue.update(willSent) } ReceiptLayoutType.CRYPTO -> { @@ -277,34 +287,34 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber totalLayout.show(true) totalTokenLayout.show(false) - fg.tvReceiptAmountValue.update("${receipt.amountCrypto} ${receipt.symbols.crypto}") - fg.tvReceiptFeeValue.update("${receipt.feeCrypto} ${receipt.symbols.crypto}") - totalLayout.tvTotalValue.update("${receipt.totalCrypto} ${receipt.symbols.crypto}") + tvReceiptAmountValue.update("${receipt.amountCrypto} ${receipt.symbols.crypto}") + tvReceiptFeeValue.update("${receipt.feeCrypto} ${receipt.symbols.crypto}") + llTotalContainer.tvTotalValue.update("${receipt.totalCrypto} ${receipt.symbols.crypto}") val willSent = SpannableStringBuilder() - .bold { - append(roughOrEmpty(receipt.willSentFiat)).append(" ") - append(receipt.symbols.fiat) - append(" (fee: ${receipt.feeFiat} ") - append(receipt.symbols.fiat).append(")") - } - totalLayout.tvWillBeSentValue.update(willSent.toString()) + .bold { + append(roughOrEmpty(receipt.willSentFiat)).append(" ") + append(receipt.symbols.fiat) + append(" (fee: ${receipt.feeFiat} ") + append(receipt.symbols.fiat).append(")") + } + llTotalContainer.tvWillBeSentValue.update(willSent.toString()) } ReceiptLayoutType.TOKEN_FIAT -> { val receipt = state.tokenFiat ?: return totalLayout.show(true) totalTokenLayout.show(false) - fg.tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}") - fg.tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}") - totalLayout.tvTotalValue.update("${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}") + tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}") + tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}") + llTotalContainer.tvTotalValue.update("${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}") val willSent = getString( R.string.send_total_subtitle_asset_format, receipt.symbols.token ?: "", receipt.willSentToken, receipt.symbols.crypto, receipt.willSentFeeCoin ) - totalLayout.tvWillBeSentValue.update(willSent) + llTotalContainer.tvWillBeSentValue.update(willSent) } ReceiptLayoutType.TOKEN_CRYPTO -> { val receipt = state.tokenCrypto ?: return @@ -312,15 +322,15 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber totalLayout.show(false) totalTokenLayout.show(true) - fg.tvReceiptAmountValue.update("${receipt.amountToken} ${receipt.symbols.token}") - fg.tvReceiptFeeValue.update("${receipt.feeCoin} ${receipt.symbols.crypto}") + tvReceiptAmountValue.update("${receipt.amountToken} ${receipt.symbols.token}") + tvReceiptFeeValue.update("${receipt.feeCoin} ${receipt.symbols.crypto}") val willSent = SpannableStringBuilder() - .bold { - append(roughOrEmpty(receipt.totalFiat)).append(" ") - append(receipt.symbols.fiat) - } - totalTokenLayout.tvTotalTokenCryptoValue.update(willSent.toString()) + .bold { + append(roughOrEmpty(receipt.totalFiat)).append(" ") + append(receipt.symbols.fiat) + } + llTotalContainer.tvTotalTokenCryptoValue.update(willSent.toString()) } } } 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..500cc39285 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt @@ -0,0 +1,40 @@ +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 FinishSuccessfulOrder : 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..464a99de96 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt @@ -0,0 +1,127 @@ +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)) + store.dispatch(ShopAction.FinishSuccessfulOrder) + } + + is ShopAction.FinishSuccessfulOrder -> { + scope.launch { + shopService.waitForCheckout( + productType = shopState.selectedProduct, + analyticsHandler = store.state.globalState.analyticsHandlers + ) + } + } + + } + +} \ 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..ee12f53109 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt @@ -0,0 +1,69 @@ +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.FinishSuccessfulOrder -> 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..51fe6cb54c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt @@ -0,0 +1,185 @@ +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 by.kirich1409.viewbindingdelegate.viewBinding +import com.tangem.tap.common.GlobalLayoutStateHandler +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 com.tangem.wallet.databinding.FragmentShopBinding +import org.rekotlin.StoreSubscriber + + +class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber { + + private val binding: FragmentShopBinding by viewBinding(FragmentShopBinding::bind) + + lateinit var keyboardObserver: KeyboardObserver + + 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 onDestroyView() { + super.onDestroyView() + keyboardObserver.unregisterListener() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupCardsImages() + setupProductSelection() + setupPromoCodeEditText() + + binding.toolbar.setNavigationOnClickListener { + requireActivity().onBackPressed() + } + + keyboardObserver = KeyboardObserver(requireActivity()).apply { + registerListener { isVisible -> + binding.flCards.show(!isVisible) + } + } + } + + private var cardTranslationY = 70f + private fun setupCardsImages() { + GlobalLayoutStateHandler(binding.imvSecond).apply { + onStateChanged = { + cardTranslationY = it.height * 0.15f + binding.imvSecond.animate() + .translationY(cardTranslationY) + .scaleX(0.9f) + .scaleY(0.9f) + .start() + binding.imvThird.animate() + .translationY(cardTranslationY * 2) + .scaleX(0.8f) + .scaleY(0.8f) + .start() + detach() + } + } + + } + + private fun setupProductSelection() = with(binding) { + chipProduct1.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_3_CARDS)) + } + chipProduct2.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_2_CARDS)) + } + } + + private fun setupPromoCodeEditText() = with(binding) { + etPromoCode.setOnEditorActionListener { view, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + val imm: InputMethodManager = + requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.windowToken, 0) + view.clearFocus() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + + etPromoCode.onFocusChangeListener = OnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + store.dispatch(ShopAction.ApplyPromoCode(etPromoCode.text.toString())) + } + } + } + + override fun newState(state: ShopState) { + if (activity == null || view == null) return + + animateProductSelection(state.selectedProduct) + handlePriceState(state) + handlePromoCodeState(state) + handleButtonsState(state) + } + + private fun animateProductSelection(selectedProduct: ProductType) { + val show = when (selectedProduct) { + ProductType.WALLET_2_CARDS -> false + ProductType.WALLET_3_CARDS -> true + } + showOrHideThirdCardWithAnimation(show) + } + + private fun showOrHideThirdCardWithAnimation(show: Boolean) = with(binding) { + val translationY = if (show) cardTranslationY * 2 else cardTranslationY + if (show) imvThird.show() + imvThird.animate() + .translationY(translationY) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + imvThird.show(show) + } + }) + } + + private fun handlePriceState(state: ShopState) = with(binding) { + tvTotal.text = state.total + tvTotalBeforeDiscount.text = state.priceBeforeDiscount + + pbPrice.show(state.total == null) + + } + + private fun handlePromoCodeState(state: ShopState) = with(binding) { + if (state.promoCode == null && !etPromoCode.hasFocus()) { + etPromoCode.setText("") + } + pbPromoCode.show(state.promoCodeLoading) + } + + private fun handleButtonsState(state: ShopState) = with(binding) { + btnPayGooglePay.root.show(state.isGooglePayAvailable) + btnAlternativePayment.show(state.isGooglePayAvailable) + btnMainAction.show(!state.isGooglePayAvailable) + + if (state.total != null) { + btnAlternativePayment.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } + btnMainAction.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } + btnPayGooglePay.root.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/redux/CurrencyListItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/CurrencyListItem.kt index 475cffff6c..8f242ed6c7 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/CurrencyListItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/CurrencyListItem.kt @@ -23,12 +23,11 @@ sealed class CurrencyListItem { blockchains: List, tokens: List, ): List { - return createBlockchainList(blockchains) + - createTokensList(R.string.add_tokens_subtitle_ethereum_tokens, Blockchain.Ethereum, tokens) + - createTokensList(R.string.add_tokens_subtitle_bsc_tokens, Blockchain.BSC, tokens) + - createTokensList(R.string.add_tokens_subtitle_binance_tokens, Blockchain.Binance, tokens) + - createTokensList(R.string.add_tokens_subtitle_polygon_tokens, Blockchain.Polygon, tokens) + - createTokensList(R.string.add_tokens_subtitle_avalanche_tokens, Blockchain.Avalanche, tokens) + val blockchainsItemList = createBlockchainList(blockchains) + val tokenItemsLists = tokens.groupBy { it.blockchain }.map { + createTokensList(R.string.add_tokens_subtitle_format, it.key, it.value) + } + return blockchainsItemList + tokenItemsLists.flatten() } private fun createBlockchainList(blockchains: List): List { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 69f9a8940e..123e5c25ff 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -49,8 +49,7 @@ class TokensMiddleware { cardFirmware = scanResponse.card.firmwareVersion, isTestNet = isTestcard ).sortedBy { it.fullName } - val currencies = - CurrencyListItem.createListOfCurrencies(blockchains, tokens).toMutableList() + val currencies = CurrencyListItem.createListOfCurrencies(blockchains, tokens) store.dispatch(TokensAction.LoadCurrencies.Success(currencies)) } 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..fd7caf6b08 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,8 @@ import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionInflater +import by.kirich1409.viewbindingdelegate.viewBinding +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 @@ -18,7 +20,7 @@ import com.tangem.tap.features.tokens.ui.adapters.CurrenciesAdapter import com.tangem.tap.mainScope import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_add_tokens.* +import com.tangem.wallet.databinding.FragmentAddTokensBinding import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* import org.rekotlin.StoreSubscriber @@ -28,6 +30,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), StoreSubscriber { private lateinit var viewAdapter: CurrenciesAdapter + private val binding: FragmentAddTokensBinding by viewBinding(FragmentAddTokensBinding::bind) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -56,29 +59,29 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), store.unsubscribe(this) } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) { super.onViewCreated(view, savedInstanceState) (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) toolbar.setNavigationOnClickListener { activity?.onBackPressed() } setupPopularTokensRecyclerView() - btn_tokens_save_changes.setOnClickListener { + btnTokensSaveChanges.setOnClickListener { store.dispatch(TokensAction.SaveChanges(viewAdapter.getAddedItems())) } } - private fun setupPopularTokensRecyclerView() { + private fun setupPopularTokensRecyclerView() = with(binding) { viewAdapter = CurrenciesAdapter() viewAdapter.setOnItemAddListener { - btn_tokens_save_changes.isEnabled = viewAdapter.getAddedItems().isNotEmpty() + btnTokensSaveChanges.isEnabled = viewAdapter.getAddedItems().isNotEmpty() } - rv_popular_tokens.layoutManager = LinearLayoutManager(context) - rv_popular_tokens.adapter = viewAdapter + rvPopularTokens.layoutManager = LinearLayoutManager(context) + rvPopularTokens.adapter = viewAdapter } override fun newState(state: TokensState) { - if (activity == null) return + if (activity == null || view == null) return viewAdapter.addedCurrencies = state.addedCurrencies viewAdapter.submitUnfilteredList(state.shownCurrencies) @@ -96,7 +99,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/tokens/ui/adapters/CurrenciesAdapter.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt index bc2b3177ae..250c6587d6 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt @@ -1,7 +1,6 @@ package com.tangem.tap.features.tokens.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter @@ -15,12 +14,13 @@ import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.loadCurrenciesIcon import com.tangem.tap.common.extensions.show import com.tangem.tap.domain.tokens.CardCurrencies +import com.tangem.tap.domain.tokens.getTokensName import com.tangem.tap.features.tokens.redux.CurrencyListItem import com.tangem.tap.features.tokens.redux.TokensAction import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.item_currency_subtitle.view.* -import kotlinx.android.synthetic.main.item_popular_token.view.* +import com.tangem.wallet.databinding.ItemCurrencySubtitleBinding +import com.tangem.wallet.databinding.ItemPopularTokenBinding import java.util.* class CurrenciesAdapter : ListAdapter(DiffUtilCallback) { @@ -61,18 +61,18 @@ class CurrenciesAdapter : ListAdapter override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { 0 -> TitleViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_currency_subtitle, parent, false) + binding = ItemCurrencySubtitleBinding + .inflate(LayoutInflater.from(parent.context), parent, false) ) 1 -> CurrenciesViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_popular_token, parent, false), - vhOnItemAddListener + binding = ItemPopularTokenBinding + .inflate(LayoutInflater.from(parent.context), parent, false), + onItemAddListener = vhOnItemAddListener ) else -> CurrenciesViewHolder( - LayoutInflater.from(parent.context) - .inflate(R.layout.item_popular_token, parent, false), - vhOnItemAddListener + binding = ItemPopularTokenBinding + .inflate(LayoutInflater.from(parent.context), parent, false), + onItemAddListener = vhOnItemAddListener ) } } @@ -108,15 +108,15 @@ class CurrenciesAdapter : ListAdapter is CurrencyListItem.BlockchainListItem -> { element.blockchain.currency.toLowerCase(Locale.US) .contains(queryNormalized) || - element.blockchain.fullName.toLowerCase(Locale.US) - .contains(queryNormalized) + element.blockchain.fullName.toLowerCase(Locale.US) + .contains(queryNormalized) } is CurrencyListItem.TokenListItem -> { element.token.name.toLowerCase(Locale.US) .contains(queryNormalized) || - element.token.symbol.toLowerCase(Locale.US) - .contains(queryNormalized) + element.token.symbol.toLowerCase(Locale.US) + .contains(queryNormalized) } is CurrencyListItem.TitleListItem -> true } @@ -128,9 +128,9 @@ class CurrenciesAdapter : ListAdapter } class CurrenciesViewHolder( - private val view: View, + private val binding: ItemPopularTokenBinding, private val onItemAddListener: ((CurrencyListItem) -> Unit), - ) : RecyclerView.ViewHolder(view) { + ) : RecyclerView.ViewHolder(binding.root) { fun bind( currency: CurrencyListItem, @@ -140,12 +140,14 @@ class CurrenciesAdapter : ListAdapter when (currency) { is CurrencyListItem.BlockchainListItem -> { val addedBlockchains = addedCurrencies?.blockchains ?: listOf() - val currentlyAdded = currentlyAddedItems.filterIsInstance() + val currentlyAdded = + currentlyAddedItems.filterIsInstance() bindBlockchain(currency, addedBlockchains, currentlyAdded) } is CurrencyListItem.TokenListItem -> { val addedTokens = addedCurrencies?.tokens ?: listOf() - val currentlyAdded = currentlyAddedItems.filterIsInstance() + val currentlyAdded = + currentlyAddedItems.filterIsInstance() bindToken(currency, addedTokens, currentlyAdded) } } @@ -155,13 +157,13 @@ class CurrenciesAdapter : ListAdapter currency: CurrencyListItem.BlockchainListItem, addedBlockchains: List, currentlyAdded: List - ) { + ) = with(binding) { val blockchain = currency.blockchain - Picasso.get().loadCurrenciesIcon(view.iv_currency, view.tv_token_letter, null, blockchain) + Picasso.get().loadCurrenciesIcon(ivCurrency, tvTokenLetter, null, blockchain) - view.tv_currency_name.text = blockchain.fullName - view.tv_currency_symbol.text = blockchain.currency - view.btn_add_token.setOnClickListener { + tvCurrencyName.text = blockchain.fullName + tvCurrencySymbol.text = blockchain.currency + btnAddToken.setOnClickListener { onItemAddListener.invoke(currency) currency.isAdded = true modifyAddTokenButton(currency) @@ -177,16 +179,17 @@ class CurrenciesAdapter : ListAdapter currency: CurrencyListItem.TokenListItem, addedTokens: List, currentlyAdded: List - ) { + ) = with(binding) { val token = currency.token - Picasso.get().loadCurrenciesIcon(view.iv_currency, view.tv_token_letter, token, token.blockchain) + Picasso.get() + .loadCurrenciesIcon(ivCurrency, tvTokenLetter, token, token.blockchain) - view.tv_currency_name.text = token.name - view.tv_currency_symbol.text = view.context.getString( + tvCurrencyName.text = token.name + tvCurrencySymbol.text = tvCurrencySymbol.getString( R.string.token_symbol_address_format, token.symbol, token.contractAddress ) - view.btn_add_token.setOnClickListener { + btnAddToken.setOnClickListener { onItemAddListener.invoke(currency) currency.isAdded = true modifyAddTokenButton(currency) @@ -199,48 +202,52 @@ class CurrenciesAdapter : ListAdapter modifyAddTokenButton(currency) } - private fun modifyAddTokenButton(currency: CurrencyListItem) { + private fun modifyAddTokenButton(currency: CurrencyListItem) = with(binding) { if (currency.isLocked) { - view.btn_add_token.setText(R.string.common_add) - view.btn_add_token.isEnabled = false + btnAddToken.setText(R.string.common_add) + btnAddToken.isEnabled = false } else { val text = if (currency.isAdded) R.string.add_token_added else R.string.common_add - view.btn_add_token.setText(text) - view.btn_add_token.isEnabled = !currency.isAdded + btnAddToken.setText(text) + btnAddToken.isEnabled = !currency.isAdded } } } - class TitleViewHolder(val view: View) : - RecyclerView.ViewHolder(view) { - fun bind(title: CurrencyListItem.TitleListItem) { - view.tv_subtitle.text = view.getString(title.titleResId).uppercase() + class TitleViewHolder(val binding: ItemCurrencySubtitleBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(title: CurrencyListItem.TitleListItem) = with(binding) { if (title.blockchain != null) { - view.cl_subtitle_container.setOnClickListener { + val subtitle = root.getString(title.titleResId, title.blockchain.getTokensName()) + tvSubtitle.text = subtitle.uppercase() + clSubtitleContainer.setOnClickListener { val rotation = if (title.isContentShown) -90f else 90f - store.dispatch(TokensAction.ToggleShowTokensForBlockchain( - title.isContentShown, title.blockchain - )) - view.iv_toggle_sublist_visibility.animate().rotation(rotation).withEndAction { - showArrow(view, title.isContentShown) + store.dispatch( + TokensAction.ToggleShowTokensForBlockchain( + title.isContentShown, title.blockchain + ) + ) + ivToggleSublistVisibility.animate().rotation(rotation).withEndAction { + showArrow(title.isContentShown) } } - showArrow(view, title.isContentShown) + showArrow(title.isContentShown) } else { - view.iv_toggle_sublist_visibility.hide() + tvSubtitle.text = root.getString(title.titleResId).uppercase() + ivToggleSublistVisibility.hide() } } - private fun showArrow(view: View, isContentShown: Boolean) { - view.iv_toggle_sublist_visibility.show() + private fun showArrow(isContentShown: Boolean) = with(binding) { + ivToggleSublistVisibility.show() val drawable = if (isContentShown) { R.drawable.ic_arrow_angle_down } else { R.drawable.ic_arrow_angle_right } - view.iv_toggle_sublist_visibility.setImageResource(drawable) - view.iv_toggle_sublist_visibility.rotation = 0f + ivToggleSublistVisibility.setImageResource(drawable) + ivToggleSublistVisibility.rotation = 0f } } } 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/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index 029b6b88b5..57a03d4313 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -9,7 +9,6 @@ import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.tokens.CardCurrencies -import com.tangem.tap.network.moonpay.MoonpayStatus import com.tangem.wallet.R import org.rekotlin.Action import java.math.BigDecimal @@ -25,11 +24,7 @@ sealed class WalletAction : Action { } - data class LoadWallet( - val moonpayStatus: MoonpayStatus? = null, - val blockchain: Blockchain? = null, - ) : - WalletAction() { + data class LoadWallet(val blockchain: Blockchain? = null) : WalletAction() { data class Success(val wallet: Wallet) : WalletAction() data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : WalletAction() data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction() @@ -146,5 +141,5 @@ sealed class WalletAction : Action { data class ChangeSelectedAddress(val type: AddressType) : WalletAction() - data class SetWalletRent(val blockchain: Blockchain, val minRent: String, val rentExempt: String): WalletAction() + data class SetWalletRent(val blockchain: Blockchain, val minRent: String, val rentExempt: String) : WalletAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 4dd980e98c..92491013ad 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -19,7 +19,7 @@ import com.tangem.tap.features.wallet.models.toPendingTransactions import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.network.moonpay.MoonpayStatus +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.store import org.rekotlin.StateType import java.math.BigDecimal @@ -160,12 +160,18 @@ data class WalletState( return updatedWallets + remainingWallets } - fun updateTradeCryptoState(moonpayStatus: MoonpayStatus?, walletData: WalletData): WalletData { - return walletData.copy(tradeCryptoState = TradeCryptoState.from(moonpayStatus, walletData)) + fun updateTradeCryptoState( + exchangeManager: CurrencyExchangeManager?, + walletData: WalletData + ): WalletData { + return walletData.copy(tradeCryptoState = TradeCryptoState.from(exchangeManager, walletData)) } - fun updateTradeCryptoState(moonpayStatus: MoonpayStatus?, walletDataList: List): List { - return walletDataList.map { it.copy(tradeCryptoState = TradeCryptoState.from(moonpayStatus, it)) } + fun updateTradeCryptoState( + exchangeManager: CurrencyExchangeManager?, + walletDataList: List + ): List { + return walletDataList.map { it.copy(tradeCryptoState = TradeCryptoState.from(exchangeManager, it)) } } fun addWalletManagers(newWalletManagers: List): WalletState { @@ -226,8 +232,8 @@ data class TradeCryptoState( val buyingAllowed: Boolean = false, ) { companion object { - fun from(moonpayStatus: MoonpayStatus?, walletData: WalletData): TradeCryptoState { - val status = moonpayStatus ?: return walletData.tradeCryptoState + fun from(exchangeManager: CurrencyExchangeManager?, walletData: WalletData): TradeCryptoState { + val status = exchangeManager ?: return walletData.tradeCryptoState val currency = walletData.currency return TradeCryptoState(status.sellIsAllowed(currency), status.buyIsAllowed(currency)) 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..19197ad893 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 @@ -1,18 +1,18 @@ package com.tangem.tap.features.wallet.redux.middlewares -import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.safeUpdate -import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.navigation.AppScreen 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.redux.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState @@ -22,6 +22,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( @@ -33,7 +34,9 @@ class MultiWalletMiddleware { when (action) { 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) { @@ -60,10 +63,7 @@ class MultiWalletMiddleware { } } store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Blockchain(action.blockchain))) - store.dispatch(WalletAction.LoadWallet( - moonpayStatus = globalState.moonpayStatus, - blockchain = action.blockchain - ) + store.dispatch(WalletAction.LoadWallet(action.blockchain) ) } is WalletAction.MultiWallet.SaveCurrencies -> { @@ -99,28 +99,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 +164,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 @@ -200,26 +205,6 @@ class MultiWalletMiddleware { } } - private fun checkForRentWarning(walletManager: WalletManager) { - val rentProvider = walletManager as? RentProvider ?: return - - scope.launch { - when (val result = rentProvider.minimalBalanceForRentExemption()) { - is Result.Success -> { - 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 - )) - } - is Result.Failure -> {} - } - } - } - private fun addTokens( tokens: List, walletState: WalletState?, 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..3667a06c35 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,22 +3,25 @@ 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 import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager +import com.tangem.tap.network.exchangeServices.buyErc20Tokens import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.launch -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) @@ -29,44 +32,38 @@ class TradeCryptoMiddleware { private fun startExchange(action: WalletAction.TradeCryptoAction) { val selectedWalletData = store.state.walletState.getSelectedWalletData() - val config = store.state.globalState.configManager?.config ?: return + val exchangeManager = store.state.globalState.currencyExchangeManager ?: return val addresses = selectedWalletData?.walletAddresses ?: return if (addresses.list.isEmpty()) return + val appCurrency = store.state.globalState.appCurrency val defaultAddress = addresses.list[0].address val currency = selectedWalletData.currency - val currencySymbol = selectedWalletData.currency?.currencySymbol ?: return + val currencySymbol = selectedWalletData.currency.currencySymbol val exchangeAction = if (action is WalletAction.TradeCryptoAction.Buy) { - TradeCryptoHelper.Action.Buy + CurrencyExchangeManager.Action.Buy } else { - TradeCryptoHelper.Action.Sell + CurrencyExchangeManager.Action.Sell } - if (exchangeAction == TradeCryptoHelper.Action.Buy && + if (exchangeAction == CurrencyExchangeManager.Action.Buy && currency is Currency.Token && currency.blockchain.isTestnet() ) { val walletManager = store.state.walletState.getWalletManager(currency.token) if (walletManager !is EthereumWalletManager) return - scope.launch { - TopUpManager().topUpTestErc20Tokens( - walletManager = walletManager, token = currency.token - ) - } + scope.launch { exchangeManager.buyErc20Tokens(walletManager, currency.token) } return } - val url = TradeCryptoHelper.getUrl( + exchangeManager.getUrl( action = exchangeAction, - blockchain = currency?.blockchain, + blockchain = currency.blockchain, cryptoCurrencyName = currencySymbol, - walletAddress = defaultAddress, - apiKey = config.moonPayApiKey, - secretKey = config.moonPayApiSecretKey - ) - Timber.d("Moonpay $exchangeAction URL: $url") - store.dispatchOnMain(NavigationAction.OpenUrl(url)) + fiatCurrency = appCurrency, + walletAddress = defaultAddress)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) } + } private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) { @@ -87,8 +84,11 @@ class TradeCryptoMiddleware { } private fun openReceiptUrl(transactionId: String) { + val exchangeManager = store.state.globalState.currencyExchangeManager ?: return + store.dispatchOnMain(NavigationAction.PopBackTo()) - val url = TradeCryptoHelper.getSellCryptoReceiptUrl(transactionId) - store.dispatchOnMain(NavigationAction.OpenUrl(url)) + exchangeManager.getSellCryptoReceiptUrl(CurrencyExchangeManager.Action.Sell, transactionId)?.let { + store.dispatchOnMain(NavigationAction.OpenUrl(it)) + } } } \ No newline at end of file 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..379008a18b 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,16 @@ 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.Attestation 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 +21,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 +44,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 -> { @@ -139,6 +148,9 @@ class WalletMiddleware { store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) } is WalletAction.LoadCardInfo -> { + val attestationFailed = action.card.attestation.status == Attestation.Status.Failed + store.dispatchOnMain(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed)) + scope.launch { val response = OnlineCardVerifier().getCardInfo( action.card.cardId, action.card.cardPublicKey @@ -148,13 +160,9 @@ class WalletMiddleware { val actionList = listOf( WalletAction.SetArtworkId(response.data.artwork?.id), WalletAction.LoadArtwork(action.card, response.data.artwork?.id), - GlobalAction.SetIfCardVerifiedOnline(response.data.passed) ) withMainContext { actionList.forEach { store.dispatch(it) } } } - is Result.Failure -> { - store.dispatchOnMain(GlobalAction.SetIfCardVerifiedOnline(false)) - } } store.dispatchOnMain(WalletAction.Warnings.CheckIfNeeded) } 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..e8ea067b6f 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 @@ -50,12 +51,12 @@ class WarningsMiddleware { } is WalletAction.Warnings.CheckRemainingSignatures -> { if (action.remainingSignatures != null && - action.remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING + action.remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING ) { store.state.globalState.warningManager - ?.removeWarnings( - messageRes = R.string.warning_low_signatures_format - ) + ?.removeWarnings( + messageRes = R.string.warning_low_signatures_format + ) addWarningMessage( warning = WarningMessagesManager.remainingSignaturesNotEnough(action.remainingSignatures), @@ -99,6 +100,9 @@ class WarningsMiddleware { addWarningMessage(WarningMessagesManager.onlineVerificationFailed()) } } + if (scanResponse.isDemoCard()) { + addWarningMessage(WarningMessagesManager.demoCardWarning()) + } setWarningMessages() } } @@ -106,7 +110,7 @@ class WarningsMiddleware { private fun showWarningLowRemainingSignaturesIfNeeded(card: Card) { val remainingSignatures = card.remainingSignatures if (remainingSignatures != null && - remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING + remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING ) { addWarningMessage( WarningMessagesManager.remainingSignaturesNotEnough( @@ -119,7 +123,7 @@ class WarningsMiddleware { private fun checkIfWarningNeeded( scanResponse: ScanResponse, ): WarningMessage? { - if (scanResponse.isTangemTwins()) return null + if (scanResponse.isTangemTwins() || scanResponse.isDemoCard()) return null if (scanResponse.card.isMultiwalletAllowed) { return if (scanResponse.card.hasSignedHashes()) { @@ -131,7 +135,7 @@ class WarningsMiddleware { } val validator = store.state.walletState.walletManagers.firstOrNull() - as? SignatureCountValidator + as? SignatureCountValidator return if (validator == null) { if (scanResponse.card.hasSignedHashes()) { WarningMessagesManager.alreadySignedHashesWarning() @@ -156,7 +160,7 @@ class WarningsMiddleware { if (scanResponse.isTangemTwins() || card.isMultiwalletAllowed) return val validator = store.state.walletState.walletManagers.firstOrNull() - as? SignatureCountValidator + as? SignatureCountValidator scope.launch { val signedHashes = card.getSingleWallet()?.totalSignedHashes ?: 0 val result = validator?.validateSignatureCount(signedHashes) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt index 1cad2a84c4..c28bea2b5b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt @@ -29,7 +29,7 @@ class OnWalletLoadedReducer { private fun onMultiWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState { val fiatCurrencySymbol = store.state.globalState.appCurrency - val moonpayStatus = store.state.globalState.moonpayStatus + val exchangeManager = store.state.globalState.currencyExchangeManager val coinAmountValue = wallet.amounts[AmountType.Coin]?.value if (walletState.getWalletData(wallet.blockchain) == null) { @@ -62,7 +62,7 @@ class OnWalletLoadedReducer { pendingTransactions = pendingTransactions.removeUnknownTransactions(), mainButton = WalletMainButton.SendButton(coinSendButton), currency = Currency.Blockchain(wallet.blockchain), - tradeCryptoState = TradeCryptoState.from(moonpayStatus, walletData) + tradeCryptoState = TradeCryptoState.from(exchangeManager, walletData) ) val tokens = wallet.getTokens().mapNotNull { token -> @@ -86,7 +86,7 @@ class OnWalletLoadedReducer { ), pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(), mainButton = WalletMainButton.SendButton(tokenSendButton), - tradeCryptoState = TradeCryptoState.from(moonpayStatus, tokenWalletData) + tradeCryptoState = TradeCryptoState.from(exchangeManager, tokenWalletData) ) } val newWallets = (tokens + newWalletData).mapNotNull { it } @@ -106,7 +106,7 @@ class OnWalletLoadedReducer { if (wallet.blockchain != walletState.primaryBlockchain) return walletState val fiatCurrencySymbol = store.state.globalState.appCurrency - val moonpayStatus = store.state.globalState.moonpayStatus + val exchangeManager = store.state.globalState.currencyExchangeManager val token = wallet.getFirstToken() val tokenData = if (token != null) { @@ -154,7 +154,7 @@ class OnWalletLoadedReducer { ), pendingTransactions = pendingTransactions.removeUnknownTransactions(), mainButton = WalletMainButton.SendButton(sendButtonEnabled), - tradeCryptoState = TradeCryptoState.from(moonpayStatus, walletState.primaryWallet) + tradeCryptoState = TradeCryptoState.from(exchangeManager, walletState.primaryWallet) ) val wallets = walletData?.let { listOf(walletData) } ?: emptyList() return walletState.copy( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index 7005235044..af75afcb1b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -34,7 +34,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { if (action !is WalletAction) return state.walletState - val moonpayStatus = store.state.globalState.moonpayStatus + val exchangeManager = store.state.globalState.currencyExchangeManager var newState = state.walletState when (action) { @@ -103,7 +103,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { currencySymbol = wallet.currencyData.currencySymbol, ), mainButton = WalletMainButton.SendButton(false), - tradeCryptoState = TradeCryptoState.from(action.moonpayStatus, wallet) + tradeCryptoState = TradeCryptoState.from(exchangeManager, wallet) ) } newState = newState.copy( @@ -124,11 +124,11 @@ private fun internalReduce(action: Action, state: AppState): WalletState { currencySymbol = wallet.currencyData.currencySymbol, ), mainButton = WalletMainButton.SendButton(false), - tradeCryptoState = TradeCryptoState.from(action.moonpayStatus, wallet) + tradeCryptoState = TradeCryptoState.from(exchangeManager, wallet) ) } val wallets = newState.replaceSomeWallets(newWallets) - newState = newState.copy(wallets = newState.updateTradeCryptoState(moonpayStatus, wallets)) + newState = newState.copy(wallets = newState.updateTradeCryptoState(exchangeManager, wallets)) } } is WalletAction.LoadWallet.Success -> newState = @@ -153,7 +153,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { } newState = newState.copy( state = progressState, - wallets = newState.updateTradeCryptoState(moonpayStatus, wallets) + wallets = newState.updateTradeCryptoState(exchangeManager, wallets) ) } is WalletAction.LoadWallet.Failure -> { @@ -188,7 +188,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { } newState = newState.copy( state = progressState, - wallets = newState.updateTradeCryptoState(moonpayStatus, wallets) + wallets = newState.updateTradeCryptoState(exchangeManager, wallets) ) } is WalletAction.SetArtworkId -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index 2992b2d543..42527e8528 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -1,13 +1,10 @@ package com.tangem.tap.features.wallet.ui import androidx.annotation.IdRes -import androidx.fragment.app.Fragment import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.wallet.R -import kotlinx.android.synthetic.main.card_balance.* -import kotlinx.android.synthetic.main.layout_balance.* -import kotlinx.android.synthetic.main.layout_balance_error.* +import com.tangem.wallet.databinding.CardBalanceBinding import java.math.BigDecimal enum class BalanceStatus { @@ -34,30 +31,34 @@ data class BalanceWidgetData( ) data class TokenData( - val amount: String, - val tokenSymbol: String, - val fiatAmount: String? = null, - val fiatRateString: String? = null, - val fiatRate: BigDecimal? = null, + val amount: String, + val tokenSymbol: String, + val fiatAmount: String? = null, + val fiatRateString: String? = null, + val fiatRate: BigDecimal? = null, ) class BalanceWidget( - val fragment: Fragment, - val data: BalanceWidgetData, - private val isTwinCard: Boolean, + private val binding: CardBalanceBinding, + private val fragment: WalletFragment, + private val data: BalanceWidgetData, + private val isTwinCard: Boolean, ) { fun setup() { when (data.status) { BalanceStatus.Loading -> { - fragment.l_balance.show() - fragment.l_balance_error.hide() - fragment.tv_fiat_amount.hide() + with(binding) { + lBalance.root.show() + lBalanceError.root.hide() + lBalance.tvFiatAmount.hide() + + lBalance.tvCurrency.text = data.currency + lBalance.tvAmount.text = "" + } - fragment.tv_currency.text = data.currency - fragment.tv_amount.text = "" showStatus(R.id.tv_status_loading) @@ -67,18 +68,18 @@ class BalanceWidget( showBalanceWithoutToken(data, false) } } - BalanceStatus.VerifiedOnline, BalanceStatus.TransactionInProgress -> { - fragment.l_balance.show() - fragment.l_balance_error.hide() + BalanceStatus.VerifiedOnline, BalanceStatus.TransactionInProgress -> with(binding.lBalance) { + root.show() + binding.lBalanceError.root.hide() val statusView = if (data.status == BalanceStatus.VerifiedOnline) { R.id.tv_status_verified } else { - fragment.tv_status_error.text = - fragment.getText(R.string.wallet_balance_tx_in_progress) + tvStatusError.text = + fragment.getText(R.string.wallet_balance_tx_in_progress) R.id.group_error } showStatus(statusView) - fragment.tv_status_error_message.hide() + tvStatusErrorMessage.hide() if (data.token != null) { showBalanceWithToken(data, true) @@ -86,79 +87,83 @@ class BalanceWidget( showBalanceWithoutToken(data, true) } } - BalanceStatus.Unreachable -> { - fragment.l_balance.show() - fragment.l_balance_error.hide() - fragment.tv_fiat_amount.hide() - fragment.group_base_currency.hide() + BalanceStatus.Unreachable -> with(binding.lBalance) { + root.show() + binding.lBalanceError.root.hide() + tvFiatAmount.hide() + groupBaseCurrency.hide() val currency = if (data.token != null) data.token.tokenSymbol else data.currency - fragment.tv_currency.text = currency - fragment.tv_amount.text = "" + tvCurrency.text = currency + tvAmount.text = "" - fragment.tv_status_error_message.text = data.errorMessage - fragment.tv_status_error.text = - fragment.getString(R.string.wallet_balance_blockchain_unreachable) + tvStatusErrorMessage.text = data.errorMessage + tvStatusError.text = + fragment.getString(R.string.wallet_balance_blockchain_unreachable) showStatus(R.id.group_error) - fragment.tv_status_error_message.show(!data.errorMessage.isNullOrBlank()) + tvStatusErrorMessage.show(!data.errorMessage.isNullOrBlank()) } - BalanceStatus.EmptyCard -> { - fragment.l_balance.hide() - fragment.l_balance_error.show() + BalanceStatus.EmptyCard -> with(binding.lBalanceError) { + binding.lBalance.root.hide() + binding.lBalanceError.root.show() if (isTwinCard) { - fragment.tv_error_title.text = fragment.getText(R.string.wallet_error_empty_twin_card) - fragment.tv_error_descriptions.text = fragment.getText(R.string.wallet_error_empty_twin_card_subtitle) + tvErrorTitle.text = fragment.getText(R.string.wallet_error_empty_twin_card) + tvErrorDescriptions.text = + fragment.getText(R.string.wallet_error_empty_twin_card_subtitle) } else { - fragment.tv_error_title.text = fragment.getText(R.string.wallet_error_empty_card) - fragment.tv_error_descriptions.text = fragment.getText(R.string.wallet_error_empty_card_subtitle) + tvErrorTitle.text = fragment.getText(R.string.wallet_error_empty_card) + tvErrorDescriptions.text = + fragment.getText(R.string.wallet_error_empty_card_subtitle) } } - BalanceStatus.NoAccount -> { - fragment.l_balance.hide() - fragment.l_balance_error.show() - fragment.tv_error_title.text = fragment.getText(R.string.wallet_error_no_account) - fragment.tv_error_descriptions.text = - fragment.getString( - R.string.wallet_error_no_account_subtitle_format, - data.amountToCreateAccount, data.currencySymbol - ) + BalanceStatus.NoAccount -> with(binding.lBalanceError) { + binding.lBalance.root.hide() + binding.lBalanceError.root.show() + tvErrorTitle.text = fragment.getText(R.string.wallet_error_no_account) + tvErrorDescriptions.text = + fragment.getString( + R.string.wallet_error_no_account_subtitle_format, + data.amountToCreateAccount, data.currencySymbol + ) } - BalanceStatus.UnknownBlockchain -> { - fragment.l_balance.hide() - fragment.l_balance_error.show() - fragment.tv_error_title.text = fragment.getText(R.string.wallet_error_unsupported_blockchain) - fragment.tv_error_descriptions.text = - fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle) + BalanceStatus.UnknownBlockchain -> with(binding.lBalanceError) { + binding.lBalance.root.hide() + binding.lBalanceError.root.show() + tvErrorTitle.text = + fragment.getText(R.string.wallet_error_unsupported_blockchain) + tvErrorDescriptions.text = + fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle) } } } - private fun showStatus(@IdRes viewRes: Int) { - fragment.group_error.show(viewRes == R.id.group_error) - fragment.tv_status_loading.show(viewRes == R.id.tv_status_loading) - fragment.tv_status_verified.show(viewRes == R.id.tv_status_verified) + private fun showStatus(@IdRes viewRes: Int) = with(binding.lBalance) { + groupError.show(viewRes == R.id.group_error) + tvStatusLoading.show(viewRes == R.id.tv_status_loading) + tvStatusVerified.show(viewRes == R.id.tv_status_verified) } - private fun showBalanceWithToken(data: BalanceWidgetData, showAmount: Boolean) { - fragment.group_base_currency.show() - fragment.tv_currency.text = data.token?.tokenSymbol - fragment.tv_base_currency.text = data.currency - fragment.tv_amount.text = if (showAmount) data.token?.amount else "" - fragment.tv_base_amount.text = if (showAmount) data.amount else "" + private fun showBalanceWithToken(data: BalanceWidgetData, showAmount: Boolean) = with(binding.lBalance) { + groupBaseCurrency.show() + tvCurrency.text = data.token?.tokenSymbol + tvBaseCurrency.text = data.currency + tvAmount.text = if (showAmount) data.token?.amount else "" + tvBaseAmount.text = if (showAmount) data.amount else "" if (showAmount) { - fragment.tv_fiat_amount.show() - fragment.tv_fiat_amount.text = data.token?.fiatAmount + tvFiatAmount.show() + tvFiatAmount.text = data.token?.fiatAmount } } - private fun showBalanceWithoutToken(data: BalanceWidgetData, showAmount: Boolean) { - fragment.group_base_currency.hide() - fragment.tv_currency.text = data.currency - fragment.tv_amount.text = if (showAmount) data.amount else "" - if (showAmount) { - fragment.tv_fiat_amount.show() - fragment.tv_fiat_amount.text = data.fiatAmountFormatted + private fun showBalanceWithoutToken(data: BalanceWidgetData, showAmount: Boolean) = + with(binding.lBalance) { + groupBaseCurrency.hide() + tvCurrency.text = data.currency + tvAmount.text = if (showAmount) data.amount else "" + if (showAmount) { + tvFiatAmount.show() + tvFiatAmount.text = data.fiatAmountFormatted + } } - } } \ No newline at end of file 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..57d4b733de 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 @@ -3,12 +3,15 @@ package com.tangem.tap.features.wallet.ui import android.app.Dialog import android.os.Bundle import android.view.* +import android.widget.TextView import androidx.activity.OnBackPressedCallback -import androidx.annotation.IdRes +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionInflater +import by.kirich1409.viewbindingdelegate.viewBinding import com.squareup.picasso.Picasso import com.tangem.common.extensions.guard import com.tangem.tap.common.SnackbarHandler @@ -22,21 +25,17 @@ import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter 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.* -import kotlinx.android.synthetic.main.layout_wallet_details.card_balance -import kotlinx.android.synthetic.main.layout_wallet_details_rent.* +import com.tangem.wallet.databinding.FragmentWalletDetailsBinding import org.rekotlin.StoreSubscriber -class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreSubscriber { +class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), + StoreSubscriber { private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter private var dialog: Dialog? = null + private val binding: FragmentWalletDetailsBinding by viewBinding(FragmentWalletDetailsBinding::bind) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -67,8 +66,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) - toolbar.setNavigationOnClickListener { activity?.onBackPressed() } + (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) + binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() } setupTransactionsRecyclerView() @@ -76,91 +75,98 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS } - private fun setupTransactionsRecyclerView() { + private fun setupTransactionsRecyclerView() = with(binding) { pendingTransactionAdapter = PendingTransactionsAdapter() - rv_pending_transaction.layoutManager = LinearLayoutManager(requireContext()) - rv_pending_transaction.adapter = pendingTransactionAdapter + rvPendingTransaction.layoutManager = LinearLayoutManager(requireContext()) + rvPendingTransaction.adapter = pendingTransactionAdapter } - private fun setupButtons() { - btn_confirm.text = getString(R.string.wallet_button_send) - btn_confirm.setOnClickListener { store.dispatch(WalletAction.Send()) } + private fun setupButtons() = with(binding) { + btnConfirm.text = getString(R.string.wallet_button_send) + btnConfirm.setOnClickListener { store.dispatch(WalletAction.Send()) } - btn_share.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) } + binding.lWalletDetails.btnShare.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) } - btn_trade.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Buy) } + btnTrade.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Buy) } - btn_sell.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Sell) } + btnSell.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Sell) } } override fun newState(state: WalletState) { - if (activity == null) return + if (activity == null || view == null) return if (state.selectedWallet == null) return val selectedWallet = state.getSelectedWalletData() ?: return - tv_currency_title.text = selectedWallet.currencyData.currency - val currency = selectedWallet.currency - if (currency is Currency.Token) { - tv_currency_subtitle.text = currency.blockchain.tokenDisplayName() - tv_currency_subtitle.show() - } else { - tv_currency_subtitle.hide() - } - showPendingTransactionsIfPresent(selectedWallet.pendingTransactions) + setupCurrency(selectedWallet.currencyData, selectedWallet.currency) setupAddressCard(selectedWallet) setupNoInternetHandling(state) setupBalanceData(selectedWallet.currencyData) - - btn_confirm.isEnabled = selectedWallet.mainButton.enabled - btn_copy.setOnClickListener { - selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> - store.dispatch(WalletAction.CopyAddress(addressString, requireContext())) - } - } - btn_share.setOnClickListener { - selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> - store.dispatch(WalletAction.ShareAddress(addressString, requireContext())) - } - } + setupButtons(selectedWallet) handleDialogs(state.walletDialog) handleCurrencyIcon(selectedWallet) handleWalletRent(selectedWallet.walletRent) - srl_wallet_details.setOnRefreshListener { + + binding.srlWalletDetails.setOnRefreshListener { if (selectedWallet.currencyData.status != BalanceStatus.Loading) { - store.dispatch(WalletAction.LoadWallet( - blockchain = selectedWallet.currency.blockchain - )) + store.dispatch( + WalletAction.LoadWallet( + blockchain = selectedWallet.currency.blockchain + ) + ) } } if (selectedWallet.currencyData.status != BalanceStatus.Loading) { - srl_wallet_details.isRefreshing = false + binding.srlWalletDetails.isRefreshing = false } - - btn_trade.isEnabled = selectedWallet.tradeCryptoState.buyingAllowed - btn_sell.show(selectedWallet.tradeCryptoState.sellingAllowed) } - private fun handleWalletRent(rent: WalletRent?) { + private fun setupCurrency(currencyData: BalanceWidgetData, currency: Currency) = with(binding) { + tvCurrencyTitle.text = currencyData.currency + if (currency is Currency.Token) { + binding.tvCurrencySubtitle.text = currency.blockchain.tokenDisplayName() + binding.tvCurrencySubtitle.show() + } else { + binding.tvCurrencySubtitle.hide() + } + } + + private fun setupButtons(selectedWallet: WalletData) = with(binding) { + btnConfirm.isEnabled = selectedWallet.mainButton.enabled + lWalletDetails.btnCopy.setOnClickListener { + selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> + store.dispatch(WalletAction.CopyAddress(addressString, requireContext())) + } + } + lWalletDetails.btnShare.setOnClickListener { + selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> + store.dispatch(WalletAction.ShareAddress(addressString, requireContext())) + } + } + btnTrade.isEnabled = selectedWallet.tradeCryptoState.buyingAllowed + btnSell.show(selectedWallet.tradeCryptoState.sellingAllowed) + } + + private fun handleWalletRent(rent: WalletRent?) = with(binding) { val rent = rent.guard { - l_rent_warning.hide() + lRentWarning.root.hide() return } - val warningMessage = requireContext().getString( - R.string.solana_rent_warning, rent.minRentValue, rent.rentExemptValue) - tv_rent_warning_message.text = warningMessage - l_rent_warning.show() + R.string.solana_rent_warning, rent.minRentValue, rent.rentExemptValue + ) + lRentWarning.tvRentWarningMessage.text = warningMessage + lRentWarning.root.show() } - private fun handleCurrencyIcon(wallet: WalletData) { + private fun handleCurrencyIcon(wallet: WalletData) = with(binding.lWalletDetails.lBalance) { Picasso.get().loadCurrenciesIcon( - imageView = iv_currency, - textView = tv_token_letter, + imageView = ivCurrency, + textView = tvTokenLetter, blockchain = wallet.currency.blockchain, token = (wallet.currency as? Currency.Token)?.token ) @@ -169,42 +175,48 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS private fun showPendingTransactionsIfPresent(pendingTransactions: List) { pendingTransactionAdapter.submitList(pendingTransactions) - rv_pending_transaction?.show(pendingTransactions.isNotEmpty()) + binding.rvPendingTransaction?.show(pendingTransactions.isNotEmpty()) } - private fun setupAddressCard(state: WalletData) { + private fun setupAddressCard(state: WalletData) = with(binding.lWalletDetails) { if (state.walletAddresses != null) { if (state.shouldShowMultipleAddress() && state.currency is Currency.Blockchain) { - (card_balance as? ViewGroup)?.beginDelayedTransition() - chip_group_address_type.show() - chip_group_address_type.fitChipsByGroupWidth() + (cardBalance as? ViewGroup)?.beginDelayedTransition() + chipGroupAddressType.show() + chipGroupAddressType.fitChipsByGroupWidth() - val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) - if (checkedId != View.NO_ID) chip_group_address_type.check(checkedId) + val checkedId = + MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) + if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId) - chip_group_address_type.setOnCheckedChangeListener { group, checkedId -> + chipGroupAddressType.setOnCheckedChangeListener { group, checkedId -> if (checkedId == -1) return@setOnCheckedChangeListener - val type = MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain) + val type = + MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain) type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) } } } else { - chip_group_address_type.hide() + chipGroupAddressType.hide() } - tv_address.text = state.walletAddresses.selectedAddress.address - tv_explore?.setOnClickListener { - store.dispatch(WalletAction.ExploreAddress( - state.walletAddresses.selectedAddress.exploreUrl, - requireContext())) + tvAddress.text = state.walletAddresses.selectedAddress.address + tvExplore?.setOnClickListener { + store.dispatch( + WalletAction.ExploreAddress( + state.walletAddresses.selectedAddress.exploreUrl, + requireContext() + ) + ) } - iv_qr_code.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode()) - tv_recieve_message.text = getQRReceiveMessage(tv_recieve_message.context, state.currency) + ivQrCode.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode()) + tvReceiveMessage.text = + getQRReceiveMessage(tvReceiveMessage.context, state.currency) } } private fun setupNoInternetHandling(state: WalletState) { if (state.state == ProgressState.Error) { if (state.error == ErrorType.NoInternetConnection) { - srl_wallet_details?.isRefreshing = false + binding.srlWalletDetails.isRefreshing = false (activity as? SnackbarHandler)?.showSnackbar( text = R.string.wallet_notification_no_internet, buttonTitle = R.string.common_retry @@ -215,74 +227,53 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS } } - private fun setupBalanceData(data: BalanceWidgetData) { + private fun setupBalanceData(data: BalanceWidgetData) = with(binding.lWalletDetails) { when (data.status) { BalanceStatus.Loading -> { - l_balance.show() - l_balance_error.hide() - tv_fiat_amount.hide() - tv_amount.text = "" - - showStatus(R.id.tv_status_loading) - showBalanceWithoutToken(data, false) + lBalanceError.root.hide() + lBalance.root.show() + lBalance.groupBalance.show() + lBalance.tvError.hide() + lBalance.tvAmount.text = data.amount + lBalance.tvFiatAmount.text = data.fiatAmountFormatted + lBalance.tvStatus.setLoadingStatus(R.string.wallet_balance_loading) } BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress, BalanceStatus.TransactionInProgress -> { - l_balance.show() - l_balance_error.hide() - val statusView = if (data.status == BalanceStatus.VerifiedOnline || - data.status == BalanceStatus.SameCurrencyTransactionInProgress) { - R.id.tv_status_verified - } else { - tv_status_error.text = - getText(R.string.wallet_balance_tx_in_progress) - R.id.group_error + lBalanceError.root.hide() + lBalance.root.show() + lBalance.groupBalance.show() + lBalance.tvError.hide() + lBalance.tvAmount.text = data.amount + lBalance.tvFiatAmount.text = data.fiatAmountFormatted + when (data.status) { + BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress -> { + lBalance.tvStatus.setVerifiedBalanceStatus(R.string.wallet_balance_verified) + } + else -> { + lBalance.tvStatus.setWarningStatus(R.string.wallet_balance_tx_in_progress) + } } - showStatus(statusView) - tv_status_error_message.hide() - showBalanceWithoutToken(data, true) } BalanceStatus.Unreachable -> { - l_balance.show() - l_balance_error.hide() - tv_fiat_amount.hide() - - tv_amount.text = "" - - tv_status_error_message.text = data.errorMessage - tv_status_error.text = - getString(R.string.wallet_balance_blockchain_unreachable) - - showStatus(R.id.group_error) - tv_status_error_message.show(!data.errorMessage.isNullOrBlank()) + lBalanceError.root.hide() + lBalance.root.show() + lBalance.groupBalance.hide() + lBalance.tvError.show() + lBalance.tvError.setWarningStatus(R.string.wallet_balance_blockchain_unreachable, data.errorMessage) } - BalanceStatus.NoAccount -> { - l_balance.hide() - l_balance_error.show() - tv_error_title.text = getText(R.string.wallet_error_no_account) - tv_error_descriptions.text = - getString( - R.string.wallet_error_no_account_subtitle_format, - data.amountToCreateAccount, data.currencySymbol - ) + lBalance.root.hide() + lBalanceError.root.show() + lBalanceError.tvErrorTitle.text = getText(R.string.wallet_error_no_account) + lBalanceError.tvErrorDescriptions.text = + getString( + R.string.wallet_error_no_account_subtitle_format, + data.amountToCreateAccount, data.currencySymbol + ) } } - card_pending_transaction_warning.show(data.status == BalanceStatus.SameCurrencyTransactionInProgress) - } - - private fun showStatus(@IdRes viewRes: Int) { - group_error.show(viewRes == R.id.group_error) - tv_status_loading.show(viewRes == R.id.tv_status_loading) - tv_status_verified.show(viewRes == R.id.tv_status_verified) - } - - private fun showBalanceWithoutToken(data: BalanceWidgetData, showAmount: Boolean) { - tv_amount.text = if (showAmount) data.amount else "" - if (showAmount) { - tv_fiat_amount.show() - tv_fiat_amount.text = data.fiatAmountFormatted - } + binding.cardPendingTransactionWarning.show(data.status == BalanceStatus.SameCurrencyTransactionInProgress) } private fun handleDialogs(walletDialog: StateDialog?) { @@ -323,4 +314,22 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS menu.getItem(0).isEnabled = walletCanBeRemoved } -} + private fun TextView.setWarningStatus(mainMessage: Int, error: String? = null) { + val text = getString(mainMessage).appendIfNotNull(error, "\nError: ") + setStatus(text, R.color.warning, R.drawable.ic_warning_small) + } + + private fun TextView.setVerifiedBalanceStatus(mainMessage: Int) { + setStatus(getString(mainMessage), R.color.accent, R.drawable.ic_ok) + } + + private fun TextView.setLoadingStatus(mainMessage: Int) { + setStatus(getString(mainMessage), R.color.darkGray4, null) + } + + private fun TextView.setStatus(text: String, @ColorRes color: Int, @DrawableRes drawable: Int?) { + this.text = text + setTextColor(getColor(color)) + setCompoundDrawablesWithIntrinsicBounds(drawable ?: 0, 0, 0, 0) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index f7978e35d8..9f6a876537 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionInflater +import by.kirich1409.viewbindingdelegate.viewBinding import com.squareup.picasso.Picasso import com.tangem.tangem_sdk_new.extensions.dpToPx import com.tangem.tap.MainActivity @@ -29,9 +30,7 @@ import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView import com.tangem.tap.features.wallet.ui.wallet.WalletView import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.fragment_wallet.* -import kotlinx.android.synthetic.main.fragment_wallet.toolbar -import kotlinx.android.synthetic.main.fragment_wallet_details.* +import com.tangem.wallet.databinding.FragmentWalletBinding import org.rekotlin.StoreSubscriber @@ -39,6 +38,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber) { warningsAdapter.submitList(warnings) - rv_warning_messages.show(warnings.isNotEmpty()) + binding.rvWarningMessages.show(warnings.isNotEmpty()) } private fun setupNoInternetHandling(state: WalletState) { if (state.state == ProgressState.Error) { if (state.error == ErrorType.NoInternetConnection) { - srl_wallet_details?.isRefreshing = false + binding.srlWallet.isRefreshing = false (activity as? MainActivity)?.showSnackbar( text = R.string.wallet_notification_no_internet, buttonTitle = R.string.common_retry @@ -152,7 +155,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber(DiffUtilCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransactionsViewHolder { - val layout = LayoutInflater.from(parent.context) - .inflate(R.layout.item_pending_transaction, parent, false) - return TransactionsViewHolder(layout) + val binding = ItemPendingTransactionBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return TransactionsViewHolder(binding) } override fun onBindViewHolder(holder: TransactionsViewHolder, position: Int) { @@ -36,13 +37,13 @@ class PendingTransactionsAdapter ) = oldItem == newItem } - class TransactionsViewHolder(val view: View) : - RecyclerView.ViewHolder(view) { + class TransactionsViewHolder(val binding: ItemPendingTransactionBinding) : + RecyclerView.ViewHolder(binding.root) { fun bind(transaction: PendingTransaction) { if (transaction.type == PendingTransactionType.Unknown) { - view.hide() + binding.root.hide() } val transactionDescriptionRes = when (transaction.type) { @@ -60,17 +61,16 @@ class PendingTransactionsAdapter PendingTransactionType.Outgoing -> R.drawable.ic_arrow_right_20 PendingTransactionType.Unknown -> return } - view.tv_pending_transaction.text = view.context.getString(transactionDescriptionRes) + binding.tvPendingTransaction.text = binding.root.getString(transactionDescriptionRes) - transaction.amount?.let { view.tv_pending_transaction_amount.text = "$it " } - view.tv_pending_transaction_currency.text = "${transaction.currency}" + transaction.amountUi?.let { binding.tvPendingTransactionAmount.text = "$it " } + binding.tvPendingTransactionCurrency.text = "${transaction.currency}" if (transaction.address != null) { - view.tv_pending_transaction_address.text = - view.context.getString(transactionAddressRes, transaction.address) + binding.tvPendingTransactionAddress.text = + binding.root.getString(transactionAddressRes, transaction.address) } - - view.iv_pending_transaction.setImageDrawable(view.context.getDrawableCompat(image)) + binding.ivPendingTransaction.setImageDrawable(binding.root.context.getDrawableCompat(image)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt index 4d96d05004..074d4a882b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -1,7 +1,6 @@ package com.tangem.tap.features.wallet.ui.adapters import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter @@ -9,6 +8,7 @@ import androidx.recyclerview.widget.RecyclerView import com.squareup.picasso.Picasso import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token +import com.tangem.tap.common.extensions.getString import com.tangem.tap.common.extensions.loadCurrenciesIcon import com.tangem.tap.common.extensions.show import com.tangem.tap.features.wallet.redux.Currency @@ -17,7 +17,7 @@ import com.tangem.tap.features.wallet.redux.WalletData import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.item_currency_wallet.view.* +import com.tangem.wallet.databinding.ItemCurrencyWalletBinding class WalletAdapter : ListAdapter(DiffUtilCallback) { @@ -32,8 +32,9 @@ class WalletAdapter } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletsViewHolder { - val layout = LayoutInflater.from(parent.context) - .inflate(R.layout.item_currency_wallet, parent, false) + val layout = ItemCurrencyWalletBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) return WalletsViewHolder(layout) } @@ -51,24 +52,24 @@ class WalletAdapter ) = oldItem == newItem } - class WalletsViewHolder(val view: View) : - RecyclerView.ViewHolder(view) { + class WalletsViewHolder(val binding: ItemCurrencyWalletBinding) : + RecyclerView.ViewHolder(binding.root) { - fun bind(wallet: WalletData) { - view.tv_currency.text = wallet.currencyData.currency - view.tv_amount.text = wallet.currencyData.amount?.takeWhile { !it.isWhitespace() } - view.tv_currency_symbol.text = wallet.currencyData.amount?.takeLastWhile { !it.isWhitespace() } - view.tv_amount_fiat.text = wallet.currencyData.fiatAmountFormatted - view.tv_exchange_rate.text = wallet.fiatRateString - view.card_wallet.setOnClickListener { + fun bind(wallet: WalletData) = with(binding) { + tvCurrency.text = wallet.currencyData.currency + tvAmount.text = wallet.currencyData.amount?.takeWhile { !it.isWhitespace() } + tvCurrencySymbol.text = wallet.currencyData.amount?.takeLastWhile { !it.isWhitespace() } + tvAmountFiat.text = wallet.currencyData.fiatAmountFormatted + tvExchangeRate.text = wallet.fiatRateString + cardWallet.setOnClickListener { store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet)) } val blockchain = wallet.currency.blockchain val token = (wallet.currency as? Currency.Token)?.token Picasso.get().loadCurrenciesIcon( - imageView = view.iv_currency, - textView = view.tv_token_letter, + imageView = ivCurrency, + textView = tvTokenLetter, token = token, blockchain = blockchain, ) @@ -77,16 +78,16 @@ class WalletAdapter BalanceStatus.Loading -> { hideWarning() if (wallet.currencyData.amount == null) { - view.tv_exchange_rate.text = view.context.getText(R.string.wallet_balance_loading) + tvExchangeRate.text = root.getString(R.string.wallet_balance_loading) } } BalanceStatus.TransactionInProgress -> - showWarning(view.context.getString(R.string.wallet_balance_tx_in_progress)) + showWarning(root.getString(R.string.wallet_balance_tx_in_progress)) BalanceStatus.Unreachable -> - showWarning(view.context.getString(R.string.wallet_balance_blockchain_unreachable)) + showWarning(root.getString(R.string.wallet_balance_blockchain_unreachable)) BalanceStatus.NoAccount -> - showWarning(view.context.getString(R.string.wallet_error_no_account)) + showWarning(root.getString(R.string.wallet_error_no_account)) else -> { } } @@ -94,7 +95,7 @@ class WalletAdapter private fun showWarning(message: String) { toggleWarning(true) - view.tv_status_error_message.text = message + binding.tvStatusErrorMessage.text = message } private fun hideWarning() { @@ -102,8 +103,8 @@ class WalletAdapter } private fun toggleWarning(show: Boolean) { - view.tv_exchange_rate.show(!show) - view.tv_status_error_message.show(show) + binding.tvExchangeRate.show(!show) + binding.tvStatusErrorMessage.show(show) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt index 22c2d56cf3..afcba036da 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt @@ -10,25 +10,23 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ItemDecoration import com.google.android.play.core.review.ReviewManagerFactory import com.tangem.tap.common.analytics.AnalyticsEvent -import com.tangem.tap.common.extensions.getActivity -import com.tangem.tap.common.extensions.getString -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.features.feedback.RateCanBeBetterEmail import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.layout_warning.view.* +import com.tangem.wallet.databinding.LayoutWarningBinding import timber.log.Timber class WarningMessagesAdapter : ListAdapter(DiffUtilCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WarningMessageVH { - val inflater = LayoutInflater.from(parent.context) - val layout = inflater.inflate(R.layout.layout_warning, parent, false) - return WarningMessageVH(layout) + val binding = LayoutWarningBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return WarningMessageVH(binding) } override fun onBindViewHolder(holder: WarningMessageVH, position: Int) { @@ -44,7 +42,7 @@ class WarningMessagesAdapter : ListAdapter(Dif } } -class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) { +class WarningMessageVH(val binding: LayoutWarningBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(warning: WarningMessage) { setBgColor(warning.priority) @@ -52,12 +50,12 @@ class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) { setupControlButtons(warning) } - private fun setText(warning: WarningMessage) { + private fun setText(warning: WarningMessage) = with(binding) { fun getString(resId: Int?, default: String, formatArgs: String? = null) = - if (resId == null) default else view.context.getString(resId, formatArgs) + if (resId == null) default else root.getString(resId, formatArgs) - view.tv_title.text = getString(warning.titleResId, warning.title) - view.tv_message.text = getString( + tvTitle.text = getString(warning.titleResId, warning.title) + tvMessage.text = getString( resId = warning.messageResId, default = warning.message, formatArgs = warning.messageFormatArg ) } @@ -68,17 +66,17 @@ class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) { WarningMessage.Priority.Warning -> R.color.warning_warning WarningMessage.Priority.Critical -> R.color.warning_critical } - view.card_view.setCardBackgroundColor(view.context.resources.getColor(color)) + binding.cardView.setCardBackgroundColor(binding.root.getColor(color)) } private fun setupControlButtons(warning: WarningMessage) = when (warning.type) { WarningMessage.Type.Permanent, WarningMessage.Type.TestCard -> { - view.group_controls_temporary.hide() - view.group_controls_rating.hide() + binding.groupControlsTemporary.hide() + binding.groupControlsRating.hide() } WarningMessage.Type.Temporary -> { - view.group_controls_rating.hide() - view.group_controls_temporary.show() + binding.groupControlsRating.hide() + binding.groupControlsTemporary.show() val buttonAction = when (warning.titleResId) { R.string.warning_important_security_info -> { @@ -92,29 +90,29 @@ class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) { } } } - val buttonTitle = view.getString( + val buttonTitle = binding.root.getString( warning.buttonTextId ?: R.string.how_to_got_it_button ) - view.btn_got_it.setOnClickListener (buttonAction) - view.btn_got_it.text = buttonTitle + binding.btnGotIt.setOnClickListener (buttonAction) + binding.btnGotIt.text = buttonTitle } WarningMessage.Type.AppRating -> { - view.group_controls_temporary.hide() - view.group_controls_rating.show() + binding.groupControlsTemporary.hide() + binding.groupControlsRating.show() val analyticsHandler = store.state.globalState.analyticsHandlers - view.btn_close.setOnClickListener { + binding.btnClose.setOnClickListener { analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS) store.dispatch(GlobalAction.HideWarningMessage(warning)) store.dispatch(WalletAction.Warnings.AppRating.RemindLater) } - view.btn_can_be_better.setOnClickListener { + binding.btnCanBeBetter.setOnClickListener { analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE) store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) store.dispatch(GlobalAction.HideWarningMessage(warning)) store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail())) } - view.btn_really_cool.setOnClickListener { - val activity = view.context.getActivity() ?: return@setOnClickListener + binding.btnReallyCool.setOnClickListener { + val activity = binding.root.context.getActivity() ?: return@setOnClickListener analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_POSITIVE) store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/AmountToSendDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/AmountToSendDialog.kt index 0175d55b59..5eea55091b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/AmountToSendDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/AmountToSendDialog.kt @@ -1,8 +1,9 @@ package com.tangem.tap.features.wallet.ui.dialogs import android.content.Context +import android.content.DialogInterface +import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.appcompat.view.ContextThemeWrapper import androidx.recyclerview.widget.* @@ -12,14 +13,22 @@ import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.dialog_wallet_send.* -import kotlinx.android.synthetic.main.item_wallet_amount_to_send.view.* - +import com.tangem.wallet.databinding.DialogWalletSendBinding +import com.tangem.wallet.databinding.ItemWalletAmountToSendBinding class AmountToSendDialog(context: Context) : BottomSheetDialog(context) { - init { - this.setContentView(R.layout.dialog_wallet_send) + var binding: DialogWalletSendBinding? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DialogWalletSendBinding.inflate(LayoutInflater.from(context)) + setContentView(binding!!.root) + } + + override fun setOnCancelListener(listener: DialogInterface.OnCancelListener?) { + super.setOnCancelListener(listener) + binding = null } fun show(amounts: List?) { @@ -27,15 +36,15 @@ class AmountToSendDialog(context: Context) : BottomSheetDialog(context) { store.dispatch(WalletAction.Send.Cancel) } - rv_amounts_to_send.layoutManager = LinearLayoutManager(context) + binding!!.rvAmountsToSend.layoutManager = LinearLayoutManager(context) val dividerItemDecoration = DividerItemDecoration( - ContextThemeWrapper(rv_amounts_to_send.context, R.style.AppTheme), + ContextThemeWrapper(binding!!.root.context, R.style.AppTheme), DividerItemDecoration.VERTICAL ) - rv_amounts_to_send.addItemDecoration(dividerItemDecoration) + binding!!.rvAmountsToSend.addItemDecoration(dividerItemDecoration) val viewAdapter = ChooseAmountAdapter() - rv_amounts_to_send.adapter = viewAdapter + binding!!.rvAmountsToSend.adapter = viewAdapter viewAdapter.submitList(amounts) @@ -48,9 +57,10 @@ class ChooseAmountAdapter : ListAdapter(DiffUtilCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AmountViewHolder { - val layout = LayoutInflater.from(parent.context) - .inflate(R.layout.item_wallet_amount_to_send, parent, false) - return AmountViewHolder(layout) + val binding = ItemWalletAmountToSendBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return AmountViewHolder(binding) } override fun onBindViewHolder(holder: AmountViewHolder, position: Int) { @@ -67,13 +77,13 @@ class ChooseAmountAdapter ) = oldItem == newItem } - class AmountViewHolder(val view: View) : - RecyclerView.ViewHolder(view) { + class AmountViewHolder(val binding: ItemWalletAmountToSendBinding) : + RecyclerView.ViewHolder(binding.root) { - fun bind(amount: Amount) { - view.tv_currency_symbol.text = amount.currencySymbol - view.tv_amount.text = amount.value?.toFormattedString(amount.decimals) - view.setOnClickListener { + fun bind(amount: Amount) = with(binding) { + tvCurrencySymbol.text = amount.currencySymbol + tvAmount.text = amount.value?.toFormattedString(amount.decimals) + root.setOnClickListener { store.dispatch(WalletAction.Send.Cancel) store.dispatch(WalletAction.Send(amount)) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionDialog.kt index 95ff1f65ea..93baaf29a3 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionDialog.kt @@ -1,30 +1,36 @@ package com.tangem.tap.features.wallet.ui.dialogs import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater import com.google.android.material.bottomsheet.BottomSheetDialog import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store -import com.tangem.wallet.R -import kotlinx.android.synthetic.main.dialog_wallet_trade.* +import com.tangem.wallet.databinding.DialogWalletTradeBinding class ChooseTradeActionDialog(context: Context) : BottomSheetDialog(context) { - init { - this.setContentView(R.layout.dialog_wallet_trade) + var binding: DialogWalletTradeBinding? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DialogWalletTradeBinding.inflate(LayoutInflater.from(context)) + setContentView(binding!!.root) } override fun show() { super.show() this.setOnDismissListener { + binding = null store.dispatch(WalletAction.HideDialog) } - dialog_btn_buy.setOnClickListener { + binding!!.dialogBtnBuy.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Buy) dismiss() } - dialog_btn_sell.setOnClickListener { + binding!!.dialogBtnSell.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Sell) dismiss() } 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..3fc5187741 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt @@ -0,0 +1,41 @@ +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() + dialog.onOk?.invoke() + } + }.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() + dialog.onOk?.invoke() + } + }.create() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 8eeef0afdc..7b2e9ba9ec 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -18,64 +18,64 @@ import com.tangem.tap.features.wallet.ui.adapters.WalletAdapter import com.tangem.tap.features.wallet.ui.dialogs.SignedHashesWarningDialog import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.card_balance.* -import kotlinx.android.synthetic.main.fragment_wallet.* -import kotlinx.android.synthetic.main.layout_balance_error.* -import kotlinx.android.synthetic.main.layout_wallet_long_buttons.* +import com.tangem.wallet.databinding.FragmentWalletBinding class MultiWalletView : WalletView { private var fragment: WalletFragment? = null + private var binding: FragmentWalletBinding? = null private var dialog: Dialog? = null private lateinit var walletsAdapter: WalletAdapter - override fun changeWalletView(fragment: WalletFragment) { - setFragment(fragment) + override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) { + setFragment(fragment, binding) onViewCreated() - showMultiWalletView(fragment) - setupButtons(fragment) + showMultiWalletView(binding) + setupButtons(binding) } - private fun showMultiWalletView(fragment: WalletFragment) = with(fragment) { - tv_twin_card_number.hide() - rv_pending_transaction.hide() - l_card_balance.hide() - l_address.hide() - l_buttons_short.hide() - l_buttons_long.hide() - btn_scan_multiwallet?.show() - rv_multiwallet.show() - btn_add_token.show() - setupWalletCardNumber(fragment) + private fun showMultiWalletView(binding: FragmentWalletBinding) = with(binding) { + tvTwinCardNumber.hide() + rvPendingTransaction.hide() + lCardBalance.root.hide() + lAddress.root.hide() + lButtonsShort.root.hide() + lButtonsLong.root.hide() + btnScanMultiwallet.show() + rvMultiwallet.show() + btnAddToken.show() + setupWalletCardNumber(binding) } - private fun setupWalletCardNumber(fragment: WalletFragment) = with(fragment) { + private fun setupWalletCardNumber(binding: FragmentWalletBinding) = with(binding) { val card = store.state.globalState.scanResponse?.card if (card?.backupStatus is Card.BackupStatus.Active) { val cardCount = (card.backupStatus as Card.BackupStatus.Active).cardCount + 1 - tv_twin_card_number.show() - tv_twin_card_number.text = - getString(R.string.wallet_twins_chip_format, 1, cardCount) + tvTwinCardNumber.show() + tvTwinCardNumber.text = + fragment?.getString(R.string.wallet_twins_chip_format, 1, cardCount) } else { - tv_twin_card_number.hide() + tvTwinCardNumber.hide() } } - private fun setupButtons(fragment: WalletFragment) = with(fragment) { - btn_scan_multiwallet?.setOnClickListener { store.dispatch(WalletAction.Scan) } + private fun setupButtons(binding: FragmentWalletBinding) = with(binding) { + btnScanMultiwallet.setOnClickListener { store.dispatch(WalletAction.Scan) } } - override fun setFragment(fragment: WalletFragment) { + override fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) { this.fragment = fragment + this.binding = binding } override fun removeFragment() { this.fragment = null + this.binding = null } override fun onViewCreated() { @@ -86,37 +86,42 @@ class MultiWalletView : WalletView { val fragment = fragment ?: return walletsAdapter = WalletAdapter() walletsAdapter.setHasStableIds(true) - fragment.rv_multiwallet.layoutManager = LinearLayoutManager(fragment.requireContext()) - fragment.rv_multiwallet.adapter = walletsAdapter + binding?.rvMultiwallet?.layoutManager = LinearLayoutManager(fragment.requireContext()) + binding?.rvMultiwallet?.adapter = walletsAdapter } override fun onNewState(state: WalletState) { val fragment = fragment ?: return + val binding = binding ?: return walletsAdapter.submitList(state.wallets, state.primaryBlockchain, state.primaryToken) - fragment.btn_add_token?.setOnClickListener { + binding.btnAddToken.setOnClickListener { store.dispatch(TokensAction.LoadCurrencies) store.dispatch(TokensAction.SetAddedCurrencies(state.wallets)) store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) } - handleErrorStates(state, fragment) + handleErrorStates(state = state, binding = binding, fragment = fragment) handleDialogs(state.walletDialog) } - private fun handleErrorStates(state: WalletState, fragment: WalletFragment) { + private fun handleErrorStates( + state: WalletState, + binding: FragmentWalletBinding, + fragment: WalletFragment + ) { when (state.primaryWallet?.currencyData?.status) { BalanceStatus.EmptyCard -> { showErrorState( - fragment, + binding, fragment.getText(R.string.wallet_error_empty_card), fragment.getString(R.string.wallet_error_empty_card_subtitle) ) - configureButtonsForEmptyWalletState(fragment) + configureButtonsForEmptyWalletState(binding) } BalanceStatus.UnknownBlockchain -> { showErrorState( - fragment, + binding, fragment.getText(R.string.wallet_error_unsupported_blockchain), fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle) ) @@ -125,24 +130,28 @@ class MultiWalletView : WalletView { } private fun showErrorState( - fragment: WalletFragment, errorTitle: CharSequence, errorDescription: CharSequence, - ) = with(fragment) { - l_card_balance.show() - l_balance.hide() - l_balance_error.show() - rv_multiwallet.hide() - btn_add_token.hide() - tv_error_title.text = errorTitle - tv_error_descriptions.text = errorDescription + binding: FragmentWalletBinding, errorTitle: CharSequence, errorDescription: CharSequence, + ) = with(binding) { + lCardBalance.root.show() + with(lCardBalance) { + lBalance.root.hide() + lBalanceError.root.show() + rvMultiwallet.show() + btnAddToken.hide() + lBalanceError.tvErrorTitle.text = errorTitle + lBalanceError.tvErrorDescriptions.text = errorDescription + } } - private fun configureButtonsForEmptyWalletState(fragment: WalletFragment) = with(fragment) { - btn_scan_multiwallet.hide() - l_buttons_long.show() - btn_scan_long.setOnClickListener { store.dispatch(WalletAction.Scan) } - btn_confirm_long.setOnClickListener { store.dispatch(WalletAction.CreateWallet) } - btn_confirm_long.text = fragment.getText(R.string.wallet_button_create_wallet) - } + private fun configureButtonsForEmptyWalletState(binding: FragmentWalletBinding) = + with(binding) { + btnScanMultiwallet.hide() + lButtonsLong.root.show() + lButtonsLong.btnScanLong.setOnClickListener { store.dispatch(WalletAction.Scan) } + lButtonsLong.btnConfirmLong.setOnClickListener { store.dispatch(WalletAction.CreateWallet) } + lButtonsLong.btnConfirmLong.text = + fragment?.getText(R.string.wallet_button_create_wallet) + } private fun handleDialogs(walletDialog: StateDialog?) { val fragment = fragment ?: return diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index d8ab4644df..224f717a2a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -5,7 +5,10 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import androidx.recyclerview.widget.LinearLayoutManager -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.beginDelayedTransition +import com.tangem.tap.common.extensions.fitChipsByGroupWidth +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState @@ -15,42 +18,43 @@ import com.tangem.tap.features.wallet.ui.BalanceWidget import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter -import com.tangem.tap.features.wallet.ui.dialogs.* +import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog +import com.tangem.tap.features.wallet.ui.dialogs.ChooseTradeActionDialog +import com.tangem.tap.features.wallet.ui.dialogs.SignedHashesWarningDialog import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.card_balance.* -import kotlinx.android.synthetic.main.fragment_wallet.* -import kotlinx.android.synthetic.main.layout_address.* -import kotlinx.android.synthetic.main.layout_wallet_long_buttons.* -import kotlinx.android.synthetic.main.layout_wallet_short_buttons.* +import com.tangem.wallet.databinding.FragmentWalletBinding class SingleWalletView : WalletView { private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter private var fragment: WalletFragment? = null + private var binding: FragmentWalletBinding? = null private var dialog: Dialog? = null - override fun setFragment(fragment: WalletFragment) { + override fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) { this.fragment = fragment + this.binding = binding } override fun removeFragment() { fragment = null + binding = null } - override fun changeWalletView(fragment: WalletFragment) { - setFragment(fragment) + override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) { + setFragment(fragment, binding) onViewCreated() - showSingleWalletView(fragment) + showSingleWalletView(binding) } - private fun showSingleWalletView(fragment: WalletFragment) = with(fragment) { - rv_multiwallet.hide() - btn_add_token.hide() - btn_scan_multiwallet?.hide() - rv_pending_transaction.hide() - l_card_balance.show() - l_address.show() + private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) { + rvMultiwallet.hide() + btnAddToken.hide() + btnScanMultiwallet.hide() + rvPendingTransaction.hide() + lCardBalance.root.show() + lAddress.root.show() } override fun onViewCreated() { @@ -61,17 +65,19 @@ class SingleWalletView : WalletView { private fun setupTransactionsRecyclerView() { val fragment = fragment ?: return pendingTransactionAdapter = PendingTransactionsAdapter() - fragment.rv_pending_transaction.layoutManager = LinearLayoutManager(fragment.requireContext()) - fragment.rv_pending_transaction.adapter = pendingTransactionAdapter + binding?.rvPendingTransaction?.layoutManager = + LinearLayoutManager(fragment.requireContext()) + binding?.rvPendingTransaction?.adapter = pendingTransactionAdapter } override fun onNewState(state: WalletState) { val fragment = fragment ?: return + val binding = binding ?: return state.primaryWallet ?: return - setupTwinCards(state.twinCardsState, fragment) - setupButtons(state.primaryWallet, state.isTangemTwins, fragment) - setupAddressCard(state.primaryWallet, fragment) + setupTwinCards(state.twinCardsState, binding) + setupButtons(state.primaryWallet, state.isTangemTwins, binding) + setupAddressCard(state.primaryWallet, binding) showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions) setupBalance(state, state.primaryWallet) handleDialogs(state.walletDialog, fragment) @@ -79,75 +85,81 @@ class SingleWalletView : WalletView { private fun showPendingTransactionsIfPresent(pendingTransactions: List) { pendingTransactionAdapter.submitList(pendingTransactions) - fragment?.rv_pending_transaction?.show(pendingTransactions.isNotEmpty()) + binding?.rvPendingTransaction?.show(pendingTransactions.isNotEmpty()) } private fun setupBalance(state: WalletState, primaryWallet: WalletData) { - fragment?.apply { - this.l_balance.show() - BalanceWidget(this, primaryWallet.currencyData, state.isTangemTwins).setup() + val fragment = fragment ?: return + binding?.apply { + lCardBalance.lBalance.root.show() + BalanceWidget( + binding = this.lCardBalance, + fragment = fragment, + data = primaryWallet.currencyData, + isTwinCard = state.isTangemTwins + ).setup() } } private fun setupTwinCards( - twinCardsState: TwinCardsState?, fragment: WalletFragment - ) = with(fragment) { + twinCardsState: TwinCardsState?, binding: FragmentWalletBinding + ) = with(binding) { twinCardsState?.cardNumber?.let { cardNumber -> - tv_twin_card_number.show() + tvTwinCardNumber.show() val number = when (cardNumber) { TwinCardNumber.First -> 1 TwinCardNumber.Second -> 2 } - tv_twin_card_number.text = - this.getString(R.string.wallet_twins_chip_format, number, 2) + tvTwinCardNumber.text = + fragment?.getString(R.string.wallet_twins_chip_format, number, 2) } if (twinCardsState?.cardNumber == null) { - tv_twin_card_number.hide() + tvTwinCardNumber.hide() } } private fun setupButtons( - state: WalletData, isTwinsWallet: Boolean, fragment: WalletFragment - ) = with(fragment){ + state: WalletData, isTwinsWallet: Boolean, binding: FragmentWalletBinding + ) = with(binding) { - setupButtonsType(state, fragment) + setupButtonsType(state, binding) val btnConfirm = if (state.tradeCryptoState.sellingAllowed || state.tradeCryptoState.buyingAllowed ) { - btn_confirm_short + lButtonsShort.btnConfirm } else { - btn_confirm_long + lButtonsLong.btnConfirmLong } val btnScan = if (state.tradeCryptoState.sellingAllowed || state.tradeCryptoState.buyingAllowed ) { - btn_scan_short + lButtonsShort.btnScan } else { - btn_scan_long + lButtonsLong.btnScanLong } - setupConfirmButton(state, btnConfirm, fragment, isTwinsWallet) + setupConfirmButton(state, btnConfirm, isTwinsWallet) btnScan.setOnClickListener { store.dispatch(WalletAction.Scan) } - btn_copy.setOnClickListener { + lAddress.btnCopy.setOnClickListener { state.walletAddresses?.selectedAddress?.address?.let { addressString -> - store.dispatch(WalletAction.CopyAddress(addressString, fragment.requireContext())) + store.dispatch(WalletAction.CopyAddress(addressString, fragment!!.requireContext())) } } - btn_show_qr.setOnClickListener { + lAddress.btnShowQr.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) } - setupTradeButton(fragment, state.tradeCryptoState) + setupTradeButton(binding, state.tradeCryptoState) } - private fun setupTradeButton(fragment: WalletFragment, tradeCryptoState: TradeCryptoState) { + private fun setupTradeButton(binding: FragmentWalletBinding, tradeCryptoState: TradeCryptoState) { val allowedToBuy = tradeCryptoState.buyingAllowed val allowedToSell = tradeCryptoState.sellingAllowed val action = when { @@ -160,35 +172,35 @@ class SingleWalletView : WalletView { allowedToBuy && !allowedToSell -> R.string.wallet_button_buy !allowedToBuy && allowedToSell -> R.string.wallet_button_sell allowedToBuy && allowedToSell -> R.string.wallet_button_trade - else -> R.string.wallet_button_trade + else -> R.string.wallet_button_trade } val icon = when { - allowedToBuy && !allowedToSell -> R.drawable.ic_arrow_up_short_btn - !allowedToBuy && allowedToSell -> R.drawable.ic_arrow_down_short_button - allowedToBuy && allowedToSell -> R.drawable.ic_arrows_up_down_short_btn + allowedToBuy && !allowedToSell -> R.drawable.ic_arrow_up + !allowedToBuy && allowedToSell -> R.drawable.ic_arrow_down + allowedToBuy && allowedToSell -> R.drawable.ic_arrows_up_down else -> null } - with(fragment) { - btn_trade.text = getText(text) - btn_trade.setCompoundDrawablesWithIntrinsicBounds(0, icon ?: 0, 0, 0) - btn_trade.setOnClickListener { if (action != null) store.dispatch(action) } + with(binding) { + lButtonsShort.btnTrade.text = fragment?.getText(text) + icon?.let { lButtonsShort.btnTrade.setIconResource(it) } + lButtonsShort.btnTrade.setOnClickListener { if (action != null) store.dispatch(action) } } } - private fun setupButtonsType(state: WalletData, fragment: WalletFragment) = with(fragment) { + private fun setupButtonsType(state: WalletData, binding: FragmentWalletBinding) = with(binding) { if (state.tradeCryptoState.sellingAllowed || state.tradeCryptoState.buyingAllowed ) { - l_buttons_long.hide() - l_buttons_short.show() + lButtonsLong.root.hide() + lButtonsShort.root.show() } else { - l_buttons_long.show() - l_buttons_short.hide() + lButtonsLong.root.show() + lButtonsShort.root.hide() } } private fun setupConfirmButton( - state: WalletData, btnConfirm: Button, fragment: WalletFragment, isTwinsWallet: Boolean, + state: WalletData, btnConfirm: Button, isTwinsWallet: Boolean, ) { val buttonTitle = when (state.mainButton) { is WalletMainButton.SendButton -> R.string.wallet_button_send @@ -200,7 +212,7 @@ class SingleWalletView : WalletView { } } } - btnConfirm.text = fragment.getString(buttonTitle) + btnConfirm.text = fragment?.getString(buttonTitle) btnConfirm.isEnabled = state.mainButton.enabled btnConfirm.setOnClickListener { when (state.mainButton) { @@ -211,33 +223,38 @@ class SingleWalletView : WalletView { } - private fun setupAddressCard(state: WalletData, fragment: WalletFragment) = with(fragment) { + private fun setupAddressCard(state: WalletData, binding: FragmentWalletBinding) = with(binding.lAddress) { if (state.walletAddresses != null && state.currency is Currency.Blockchain) { - l_address?.show() + binding.lAddress.root.show() if (state.shouldShowMultipleAddress()) { - (l_address as? ViewGroup)?.beginDelayedTransition() - chip_group_address_type.show() - chip_group_address_type.fitChipsByGroupWidth() + (binding.lAddress.root as? ViewGroup)?.beginDelayedTransition() + chipGroupAddressType.show() + chipGroupAddressType.fitChipsByGroupWidth() - val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) - if (checkedId != View.NO_ID) chip_group_address_type.check(checkedId) + val checkedId = + MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) + if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId) - chip_group_address_type.setOnCheckedChangeListener { group, checkedId -> + chipGroupAddressType.setOnCheckedChangeListener { group, checkedId -> if (checkedId == -1) return@setOnCheckedChangeListener - val type = MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain) + val type = + MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain) type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) } } } else { - chip_group_address_type.hide() + chipGroupAddressType.hide() } - tv_address.text = state.walletAddresses.selectedAddress.address - tv_explore?.setOnClickListener { - store.dispatch(WalletAction.ExploreAddress( + tvAddress.text = state.walletAddresses.selectedAddress.address + tvExplore.setOnClickListener { + store.dispatch( + WalletAction.ExploreAddress( state.walletAddresses.selectedAddress.exploreUrl, - fragment.requireContext())) + fragment!!.requireContext() + ) + ) } } else { - l_address?.hide() + binding.lAddress.root.hide() } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt index ab49865c02..1d9ea0eac0 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt @@ -2,14 +2,15 @@ package com.tangem.tap.features.wallet.ui.wallet import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.ui.WalletFragment +import com.tangem.wallet.databinding.FragmentWalletBinding interface WalletView { - fun setFragment(fragment: WalletFragment) + fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) fun removeFragment() - fun changeWalletView(fragment: WalletFragment) + fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) fun onViewCreated() diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt new file mode 100644 index 0000000000..a6e9c55cba --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -0,0 +1,136 @@ +package com.tangem.tap.network.exchangeServices + +import com.tangem.Message +import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain +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.CryptoCurrencyName +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.TangemSigner +import com.tangem.tap.store +import com.tangem.tap.tangemSdk +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +interface ExchangeService { + suspend fun isBuyAllowed(): Boolean + suspend fun availableToBuy(): List + suspend fun isSellAllowed(): Boolean + suspend fun availableToSell(): List +} + +interface ExchangeUrlBuilder { + fun getUrl( + action: CurrencyExchangeManager.Action, + blockchain: Blockchain, + cryptoCurrencyName: CryptoCurrencyName, + fiatCurrency: String, + walletAddress: String, + ): String? + + fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? + + companion object { + const val SCHEME = "https" + const val URL_SELL = "sell.moonpay.com" + const val SUCCESS_URL = "tangem://success.tangem.com" + } +} + +class CurrencyExchangeManager( + private val onramperService: ExchangeService, + private val moonPayService: ExchangeService, +) : ExchangeService, ExchangeUrlBuilder { + + var status: CurrencyExchangeStatus? = null + private set + + suspend fun getStatus(): CurrencyExchangeStatus { + val isBuyAllowed = isBuyAllowed() + val isSellAllowed = isSellAllowed() + val availableToBuy = availableToBuy() + val availableToSell = availableToSell() + status = CurrencyExchangeStatus( + isBuyAllowed, + isSellAllowed, + availableToBuy, + availableToSell, + ) + return status!! + } + + override suspend fun isBuyAllowed(): Boolean = onramperService.isBuyAllowed() + override suspend fun availableToBuy(): List = onramperService.availableToBuy() + override suspend fun isSellAllowed(): Boolean = moonPayService.isSellAllowed() + override suspend fun availableToSell(): List = moonPayService.availableToSell() + + override fun getUrl( + action: Action, + blockchain: Blockchain, + cryptoCurrencyName: CryptoCurrencyName, + fiatCurrency: String, + walletAddress: String, + ): String? { + if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl() + + val urlBuilder = getExchangeUrlBuilder(action) + return urlBuilder.getUrl(action, blockchain, cryptoCurrencyName, fiatCurrency, walletAddress) + } + + override fun getSellCryptoReceiptUrl(action: Action, transactionId: String): String? { + val urlBuilder = getExchangeUrlBuilder(action) + return urlBuilder.getSellCryptoReceiptUrl(action, transactionId) + } + + private fun getExchangeUrlBuilder(action: Action): ExchangeUrlBuilder { + return when (action) { + Action.Buy -> onramperService + Action.Sell -> moonPayService + } as ExchangeUrlBuilder + } + + enum class Action { Buy, Sell } +} + +data class CurrencyExchangeStatus( + val isBuyAllowed: Boolean, + val isSellAllowed: Boolean, + val availableToBuy: List, + val availableToSell: List, +) + +suspend fun CurrencyExchangeManager.buyErc20Tokens(walletManager: EthereumWalletManager, token: Token) { + walletManager.safeUpdate() + + val amountToSend = Amount(walletManager.wallet.blockchain) + val destinationAddress = token.contractAddress + + val feeResult = walletManager.getFee(amountToSend, + destinationAddress) as? Result.Success ?: return + val fee = feeResult.data[0] + + if ((walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO) < fee.value) { + return + } + + val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress) + + val signer = TangemSigner( + tangemSdk = tangemSdk, Message() + ) { signResponse -> + store.dispatch( + GlobalAction.UpdateWalletSignedHashes( + walletSignedHashes = signResponse.totalSignedHashes, + walletPublicKey = walletManager.wallet.publicKey.seedKey, + remainingSignatures = signResponse.remainingSignatures + ) + ) + } + walletManager.send(transaction, signer) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayApi.kt new file mode 100644 index 0000000000..37dbe6185a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayApi.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.network.exchangeServices.moonpay + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import retrofit2.http.GET +import retrofit2.http.Query + +interface MoonPayApi { + + @GET(MOOONPAY_IP_ADDRESS_REQUEST_URL) + suspend fun getUserStatus( + @Query("apiKey") moonPayApiKey: String, + ): MoonPayUserStatus + + @GET(MOOONPAY_CURRENCIES_REQUEST_URL) + suspend fun getCurrencies( + @Query("apiKey") moonPayApiKey: String, + ): List + + + companion object { + const val MOOONPAY_BASE_URL = "https://api.moonpay.com/" + const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "v4/ip_address/" + const val MOOONPAY_CURRENCIES_REQUEST_URL = "v3/currencies/" + } +} + + +@JsonClass(generateAdapter = true) +data class MoonPayUserStatus( + val isBuyAllowed: Boolean, + val isSellAllowed: Boolean, + @Json(name = "isAllowed") + val isMoonpayAllowed: Boolean, + @Json(name = "alpha3") + val countryCode: String, + @Json(name = "state") + val stateCode: String, +) + +@JsonClass(generateAdapter = true) +data class MoonPayCurrencies( + val type: String, + val code: String, + val supportsLiveMode: Boolean = false, + val isSuspended: Boolean = true, + val isSupportedInUS: Boolean = false, + val isSellSupported: Boolean = false, + val notAllowedUSStates: List = emptyList(), +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt new file mode 100644 index 0000000000..f3ca64c4db --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -0,0 +1,142 @@ +package com.tangem.tap.network.exchangeServices.moonpay + +import android.net.Uri +import android.util.Base64 +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.services.Result +import com.tangem.common.services.performRequest +import com.tangem.tap.common.extensions.urlEncode +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.network.createRetrofitInstance +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager +import com.tangem.tap.network.exchangeServices.ExchangeService +import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder +import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME +import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.URL_SELL +import kotlinx.coroutines.coroutineScope +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +class MoonPayService( + private val apiKey: String, + private val secretKey: String, +) : ExchangeService, ExchangeUrlBuilder { + + private val api: MoonPayApi by lazy { + createRetrofitInstance(MoonPayApi.MOOONPAY_BASE_URL) + .create(MoonPayApi::class.java) + } + + private var status: MoonPayStatus? = null + + private suspend fun updateStatus() { + try { + coroutineScope { + val userStatusResult = performRequest { api.getUserStatus(apiKey) } + if (userStatusResult is Result.Failure) return@coroutineScope userStatusResult + + val currenciesResult = performRequest { api.getCurrencies(apiKey) } + if (currenciesResult is Result.Failure) return@coroutineScope currenciesResult + + val userStatus = (userStatusResult as Result.Success).data + val currencies = (currenciesResult as Result.Success).data + +// val currenciesToBuy = mutableListOf() + val currenciesToSell = mutableListOf() + + currencies.forEach { currencyStatus -> + if (currencyStatus.type != "crypto" || currencyStatus.isSuspended || + !currencyStatus.supportsLiveMode + ) { + return@forEach + } + + if (userStatus.countryCode == "USA") { + if (!currencyStatus.isSupportedInUS) return@forEach + if (currencyStatus.notAllowedUSStates.contains(userStatus.stateCode)) return@forEach + } + val currencyCode = currencyStatus.code.uppercase() +// currenciesToBuy.add(currencyCode) + + if (currencyStatus.isSellSupported) currenciesToSell.add(currencyCode) + } +// currenciesToBuy.sort() + currenciesToSell.sort() + + status = MoonPayStatus(currenciesToSell, userStatus, currencies) + } + } catch (error: Error) { + status = null + Result.Failure(error) + } + } + + override suspend fun isBuyAllowed(): Boolean = false + + override suspend fun availableToBuy(): List = listOf() + + override suspend fun isSellAllowed(): Boolean { + refreshStatus() + return status?.responseUserStatus?.isSellAllowed ?: false + } + + override suspend fun availableToSell(): List { + refreshStatus() + return status?.availableToSell ?: emptyList() + } + + private suspend fun refreshStatus() { + if (status == null) { + updateStatus() + } + } + + override fun getUrl( + action: CurrencyExchangeManager.Action, + blockchain: Blockchain, + cryptoCurrencyName: CryptoCurrencyName, + fatCurrency: String, + walletAddress: String + ): String? { + if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException() + + val uri = Uri.Builder() + .scheme(SCHEME) + .authority(URL_SELL) + .appendQueryParameter("apiKey", apiKey.urlEncode()) + .appendQueryParameter("baseCurrencyCode", cryptoCurrencyName.urlEncode()) + .appendQueryParameter("refundWalletAddress", walletAddress.urlEncode()) + .appendQueryParameter("redirectURL", "tangem://sell-request.tangem.com".urlEncode()) + + val originalQuery = uri.build().toString() + val signature = createSignature(originalQuery) + uri.appendQueryParameter("signature", signature.urlEncode()) + + val url = uri.build().toString() + return url + } + + override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? { + val url = Uri.Builder() + .scheme(SCHEME) + .authority(URL_SELL) + .appendPath("transaction_receipt") + .appendQueryParameter("transactionId", transactionId).build().toString() + return url + + } + + private fun createSignature(data: String): String { + val sha256Hmac = Mac.getInstance("HmacSHA256") + val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256") + sha256Hmac.init(secretKey) + val sha256encoded = sha256Hmac.doFinal(data.toByteArray()) + return Base64.encodeToString(sha256encoded, Base64.NO_WRAP) + } +} + +private data class MoonPayStatus( + val availableToSell: List, + val responseUserStatus: MoonPayUserStatus, + val responseCurrencies: List +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt new file mode 100644 index 0000000000..01fec5d3d9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt @@ -0,0 +1,94 @@ +package com.tangem.tap.network.exchangeServices.onramper + +import com.squareup.moshi.JsonClass +import okhttp3.Interceptor +import okhttp3.Response +import retrofit2.http.GET +import retrofit2.http.Path + +interface OnramperApi { + @GET("gateways") + suspend fun gateways(): GatewaysResponse + + @GET("rate/{fromCurrency}/{toCurrency}/{paymentMethod}/{amount}") + suspend fun rate( + @Path("fromCurrency") fromCurrency: String, + @Path("toCurrency") toCurrency: String, + @Path("paymentMethod") paymentMethod: String, + @Path("amount") amount: Int, + ): RateResponse + + companion object { + val BASE_URL = "https://onramper.tech/" + } +} + +class AddKeyToHeaderInterceptor( + private val key: String +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request().newBuilder().addHeader("Authorization", "Basic $key").build() + return chain.proceed(request) + } +} + +@JsonClass(generateAdapter = true) +data class GatewaysResponse( + val gateways: List +) + +@JsonClass(generateAdapter = true) +data class OnramperGateway( + val identifier: String, + val paymentMethods: List, + val fiatCurrencies: List, + val cryptoCurrencies: List +) + +@JsonClass(generateAdapter = true) +data class OnramperCurrency( + val id: String, + val code: String, + val precision: Int +) + +@JsonClass(generateAdapter = true) +data class RateResponse( + val identifier: String, + val duration: OnramperDuration, + val available: Boolean, + val error: OnramperError? = null, + val rate: Double? = null, + val fees: Double? = null, + val requiredKYC: List? = null, + val receivedCrypto: Double? = null, + val nextStep: OnramperNextStep? = null, +) + +@JsonClass(generateAdapter = true) +data class OnramperNextStep( + val type: String, + val url: String, + val message: String, + val extraData: List +) + +@JsonClass(generateAdapter = true) +data class OnramperExtraData( + val type: String, + val name: String, + val humanName: String, +) + +@JsonClass(generateAdapter = true) +data class OnramperDuration( + val seconds: Long, + val message: String +) + +@JsonClass(generateAdapter = true) +data class OnramperError( + val type: String, + val message: String, + val limit: Double +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt new file mode 100644 index 0000000000..a2534b966d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt @@ -0,0 +1,111 @@ +package com.tangem.tap.network.exchangeServices.onramper + +import android.net.Uri +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.services.Result +import com.tangem.common.services.performRequest +import com.tangem.tap.common.extensions.urlEncode +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.network.createRetrofitInstance +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager +import com.tangem.tap.network.exchangeServices.ExchangeService +import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder +import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME +import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SUCCESS_URL +import kotlinx.coroutines.coroutineScope +import java.util.* + +/** +[REDACTED_AUTHOR] + */ +class OnramperService( + val apiKey: String +) : ExchangeService, ExchangeUrlBuilder { + + private val api: OnramperApi by lazy { + createRetrofitInstance(OnramperApi.BASE_URL, listOf(AddKeyToHeaderInterceptor(apiKey))) + .create(OnramperApi::class.java) + } + + private var status: OnramperStatus? = null + + private suspend fun updateStatus() { + try { + coroutineScope { + val result = performRequest { api.gateways() } + if (result is Result.Failure) return@coroutineScope result + + val response = (result as Result.Success).data + val currenciesToBuy = extractCurrenciesToBuy(response).sorted() + val status = OnramperStatus(currenciesToBuy, response) + this@OnramperService.status = status + } + } catch (error: Error) { + status = null + Result.Failure(error) + } + } + + private fun extractCurrenciesToBuy(response: GatewaysResponse): List { + return response.gateways.map { gateway -> + gateway.cryptoCurrencies.map { currency -> currency.code } + }.flatten().toMutableSet().toList() + } + + override suspend fun isBuyAllowed(): Boolean { + refreshStatus() + return status != null + } + + override suspend fun availableToBuy(): List { + refreshStatus() + return status?.availableToBuy ?: emptyList() + } + + private suspend fun refreshStatus() { + if (status == null) { + updateStatus() + } + } + + override suspend fun isSellAllowed(): Boolean = false + + override suspend fun availableToSell(): List = listOf() + + override fun getUrl( + action: CurrencyExchangeManager.Action, + blockchain: Blockchain, + cryptoCurrencyName: CryptoCurrencyName, + fiatCurrency: String, + walletAddress: String, + ): String? { + var languageCode = Locale.getDefault().language + if (languageCode.isEmpty()) languageCode = "en" + + val builder = Uri.Builder() + .scheme(SCHEME) + .authority("widget.onramper.com") + .appendQueryParameter("apiKey", this.apiKey.urlEncode()) + .appendQueryParameter("defaultCrypto", cryptoCurrencyName) + .appendQueryParameter("wallets", "${blockchain.currency}:$walletAddress".urlEncode()) + .appendQueryParameter("redirectURL", SUCCESS_URL) + .appendQueryParameter("defaultFiat", fiatCurrency) + .appendQueryParameter("language", languageCode) + + status?.apply { + val gateways = responseGateways.gateways.joinToString(",") { it.identifier }.urlEncode() + builder.appendQueryParameter("onlyGateways", gateways) + + } + + val url = builder.build().toString() + return url + } + + override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null +} + +private data class OnramperStatus( + val availableToBuy: List, + val responseGateways: GatewaysResponse +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/moonpay/MoonpayApi.kt b/app/src/main/java/com/tangem/tap/network/moonpay/MoonpayApi.kt deleted file mode 100644 index 5d1bfbb5c2..0000000000 --- a/app/src/main/java/com/tangem/tap/network/moonpay/MoonpayApi.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.network.moonpay - -import retrofit2.http.GET -import retrofit2.http.Query - -interface MoonpayApi { - @GET(MOOONPAY_IP_ADDRESS_REQUEST_URL) - suspend fun getUserStatus( - @Query("apiKey") moonpayApiKey: String, - ): MoonPayUserStatus - - @GET(MOOONPAY_CURRENCIES_REQUEST_URL) - suspend fun getCurrencies( - @Query("apiKey") moonpayApiKey: String, - ): List - - - companion object { - const val MOOONPAY_BASE_URL = "https://api.moonpay.com/" - const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "v4/ip_address/" - const val MOOONPAY_CURRENCIES_REQUEST_URL = "v3/currencies/" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/moonpay/MoonpayService.kt b/app/src/main/java/com/tangem/tap/network/moonpay/MoonpayService.kt deleted file mode 100644 index 3e9696700d..0000000000 --- a/app/src/main/java/com/tangem/tap/network/moonpay/MoonpayService.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.tap.network.moonpay - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import com.tangem.common.services.Result -import com.tangem.common.services.performRequest -import com.tangem.tap.network.createRetrofitInstance -import kotlinx.coroutines.coroutineScope - -class MoonpayService { - - private val moonpayApi: MoonpayApi by lazy { - createRetrofitInstance(MoonpayApi.MOOONPAY_BASE_URL) - .create(MoonpayApi::class.java) - } - - suspend fun getMoonpayStatus(moonpayApiKey: String): Result { - return try { - coroutineScope { - val userStatusResult = performRequest { moonpayApi.getUserStatus(moonpayApiKey) } - if (userStatusResult is Result.Failure) return@coroutineScope userStatusResult - - val currenciesResult = performRequest { moonpayApi.getCurrencies(moonpayApiKey) } - if (currenciesResult is Result.Failure) return@coroutineScope currenciesResult - - val userStatus = (userStatusResult as Result.Success).data - val currencies = (currenciesResult as Result.Success).data - - val currenciesToBuy = mutableListOf() - val currenciesToSell = mutableListOf() - - currencies.forEach { currencyStatus -> - if (currencyStatus.type != "crypto" || currencyStatus.isSuspended || - !currencyStatus.supportsLiveMode - ) { - return@forEach - } - - if (userStatus.countryCode == "USA") { - if (!currencyStatus.isSupportedInUS) return@forEach - if (currencyStatus.notAllowedUSStates.contains(userStatus.stateCode)) return@forEach - } - val currencyCode = currencyStatus.code.uppercase() - currenciesToBuy.add(currencyCode) - - if (currencyStatus.isSellSupported) currenciesToSell.add(currencyCode) - } - - Result.Success(MoonpayStatus( - isBuyAllowed = userStatus.isBuyAllowed, - isSellAllowed = userStatus.isSellAllowed, - availableToBuy = if (userStatus.isBuyAllowed) currenciesToBuy else emptyList(), - availableToSell = if (userStatus.isSellAllowed) currenciesToSell else emptyList() - )) - } - } catch (error: Error) { - Result.Failure(error) - } - } -} - -data class MoonpayStatus( - val isBuyAllowed: Boolean, - val isSellAllowed: Boolean, - val availableToBuy: List, - val availableToSell: List, -) - -@JsonClass(generateAdapter = true) -data class MoonPayUserStatus( - val isBuyAllowed: Boolean, - val isSellAllowed: Boolean, - @Json(name = "isAllowed") - val isMoonpayAllowed: Boolean, - @Json(name = "alpha3") - val countryCode: String, - @Json(name = "state") - val stateCode: String, -) - -@JsonClass(generateAdapter = true) -data class MoonpayCurrencies( - val type: String, - val code: String, - val supportsLiveMode: Boolean = false, - val isSuspended: Boolean = true, - val isSupportedInUS: Boolean = false, - val isSellSupported: Boolean = false, - val notAllowedUSStates: List = emptyList(), -) \ 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-hdpi/floating_cards.png b/app/src/main/res/drawable-hdpi/floating_cards.png new file mode 100644 index 0000000000..6f8eba908f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/floating_cards.png differ diff --git a/app/src/main/res/drawable-hdpi/meet_tangem.png b/app/src/main/res/drawable-hdpi/meet_tangem.png new file mode 100644 index 0000000000..25f2d5a3ba Binary files /dev/null and b/app/src/main/res/drawable-hdpi/meet_tangem.png differ diff --git a/app/src/main/res/drawable-hdpi/revolutionary_wallet.png b/app/src/main/res/drawable-hdpi/revolutionary_wallet.png new file mode 100644 index 0000000000..f4f384a54d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/revolutionary_wallet.png differ diff --git a/app/src/main/res/drawable-hdpi/thousands_of_currencies.png b/app/src/main/res/drawable-hdpi/thousands_of_currencies.png new file mode 100644 index 0000000000..af3bf1ca51 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/thousands_of_currencies.png differ diff --git a/app/src/main/res/drawable-hdpi/wallet_for_everyone.png b/app/src/main/res/drawable-hdpi/wallet_for_everyone.png new file mode 100644 index 0000000000..6eec2f53dd Binary files /dev/null and b/app/src/main/res/drawable-hdpi/wallet_for_everyone.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-mdpi/floating_cards.png b/app/src/main/res/drawable-mdpi/floating_cards.png new file mode 100644 index 0000000000..9ff5cb944a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/floating_cards.png differ diff --git a/app/src/main/res/drawable-mdpi/meet_tangem.png b/app/src/main/res/drawable-mdpi/meet_tangem.png new file mode 100644 index 0000000000..ef04e35830 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/meet_tangem.png differ diff --git a/app/src/main/res/drawable-mdpi/revolutionary_wallet.png b/app/src/main/res/drawable-mdpi/revolutionary_wallet.png new file mode 100644 index 0000000000..4dba3bffa4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/revolutionary_wallet.png differ diff --git a/app/src/main/res/drawable-mdpi/thousands_of_currencies.png b/app/src/main/res/drawable-mdpi/thousands_of_currencies.png new file mode 100644 index 0000000000..4f448c7df8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/thousands_of_currencies.png differ diff --git a/app/src/main/res/drawable-mdpi/wallet_for_everyone.png b/app/src/main/res/drawable-mdpi/wallet_for_everyone.png new file mode 100644 index 0000000000..9ec8c92210 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/wallet_for_everyone.png differ diff --git a/app/src/main/res/drawable-mdpi/web_3.png b/app/src/main/res/drawable-mdpi/web_3.png new file mode 100644 index 0000000000..df3245ff41 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/web_3.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/floating_cards.png b/app/src/main/res/drawable-xhdpi/floating_cards.png new file mode 100644 index 0000000000..3445299a6f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/floating_cards.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-xhdpi/meet_tangem.png b/app/src/main/res/drawable-xhdpi/meet_tangem.png new file mode 100644 index 0000000000..958f83e0a6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/meet_tangem.png differ diff --git a/app/src/main/res/drawable-xhdpi/revolutionary_wallet.png b/app/src/main/res/drawable-xhdpi/revolutionary_wallet.png new file mode 100644 index 0000000000..e72d2ff281 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/revolutionary_wallet.png differ diff --git a/app/src/main/res/drawable-xhdpi/thousands_of_currencies.png b/app/src/main/res/drawable-xhdpi/thousands_of_currencies.png new file mode 100644 index 0000000000..cd226e8bf3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/thousands_of_currencies.png differ diff --git a/app/src/main/res/drawable-xhdpi/wallet_for_everyone.png b/app/src/main/res/drawable-xhdpi/wallet_for_everyone.png new file mode 100644 index 0000000000..773a3bb463 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/wallet_for_everyone.png differ diff --git a/app/src/main/res/drawable-xhdpi/web_3.png b/app/src/main/res/drawable-xhdpi/web_3.png new file mode 100644 index 0000000000..8af4c9c432 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/web_3.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/floating_cards.png b/app/src/main/res/drawable-xxhdpi/floating_cards.png new file mode 100644 index 0000000000..000db85171 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/floating_cards.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-xxhdpi/meet_tangem.png b/app/src/main/res/drawable-xxhdpi/meet_tangem.png new file mode 100644 index 0000000000..4d5144346d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/meet_tangem.png differ diff --git a/app/src/main/res/drawable-xxhdpi/revolutionary_wallet.png b/app/src/main/res/drawable-xxhdpi/revolutionary_wallet.png new file mode 100644 index 0000000000..83a8bc7963 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/revolutionary_wallet.png differ diff --git a/app/src/main/res/drawable-xxhdpi/thousands_of_currencies.png b/app/src/main/res/drawable-xxhdpi/thousands_of_currencies.png new file mode 100644 index 0000000000..572bb12ae8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/thousands_of_currencies.png differ diff --git a/app/src/main/res/drawable-xxhdpi/wallet_for_everyone.png b/app/src/main/res/drawable-xxhdpi/wallet_for_everyone.png new file mode 100644 index 0000000000..18b099256b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/wallet_for_everyone.png differ diff --git a/app/src/main/res/drawable-xxhdpi/web_3.png b/app/src/main/res/drawable-xxhdpi/web_3.png new file mode 100644 index 0000000000..8aa6f0b237 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/web_3.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/floating_cards.png b/app/src/main/res/drawable-xxxhdpi/floating_cards.png new file mode 100644 index 0000000000..81dd5e3119 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/floating_cards.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-xxxhdpi/meet_tangem.png b/app/src/main/res/drawable-xxxhdpi/meet_tangem.png new file mode 100644 index 0000000000..a84bb7b804 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/meet_tangem.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/revolutionary_wallet.png b/app/src/main/res/drawable-xxxhdpi/revolutionary_wallet.png new file mode 100644 index 0000000000..bc0d94be7f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/revolutionary_wallet.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/thousands_of_currencies.png b/app/src/main/res/drawable-xxxhdpi/thousands_of_currencies.png new file mode 100644 index 0000000000..5e2406e2c7 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/thousands_of_currencies.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/wallet_for_everyone.png b/app/src/main/res/drawable-xxxhdpi/wallet_for_everyone.png new file mode 100644 index 0000000000..1d99d407ab Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/wallet_for_everyone.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/web_3.png b/app/src/main/res/drawable-xxxhdpi/web_3.png new file mode 100644 index 0000000000..988c52d448 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/web_3.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_overlay.xml b/app/src/main/res/drawable/ic_overlay.xml new file mode 100644 index 0000000000..586397f855 --- /dev/null +++ b/app/src/main/res/drawable/ic_overlay.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_promo_code.xml b/app/src/main/res/drawable/ic_promo_code.xml new file mode 100644 index 0000000000..149d59df21 --- /dev/null +++ b/app/src/main/res/drawable/ic_promo_code.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_shipping.xml b/app/src/main/res/drawable/ic_shipping.xml new file mode 100644 index 0000000000..b36ffe1bed --- /dev/null +++ b/app/src/main/res/drawable/ic_shipping.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/shape_line.xml b/app/src/main/res/drawable/shape_line.xml new file mode 100644 index 0000000000..cf7b6cfb0f --- /dev/null +++ b/app/src/main/res/drawable/shape_line.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rectangle_rounded_4.xml b/app/src/main/res/drawable/shape_rectangle_rounded_4.xml new file mode 100644 index 0000000000..b7d03c2907 --- /dev/null +++ b/app/src/main/res/drawable/shape_rectangle_rounded_4.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/buy_with_googlepay_button.xml b/app/src/main/res/layout/buy_with_googlepay_button.xml new file mode 100755 index 0000000000..2553181fd8 --- /dev/null +++ b/app/src/main/res/layout/buy_with_googlepay_button.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/main/res/layout/dialog_onboarding_address_info.xml b/app/src/main/res/layout/dialog_onboarding_address_info.xml index 55617e452f..ca04440067 100644 --- a/app/src/main/res/layout/dialog_onboarding_address_info.xml +++ b/app/src/main/res/layout/dialog_onboarding_address_info.xml @@ -24,7 +24,7 @@ app:layout_constraintTop_toBottomOf="@+id/pseudo_toolbar" /> + app:title="@string/add_tokens_title" /> @@ -51,7 +51,7 @@ android:layout_marginBottom="16dp" android:enabled="false" android:gravity="center" - android:text="Save changes" + android:text="@string/common_save_changes" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index b6475a8ac3..58ac038846 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -133,32 +133,39 @@ app:layout_constraintTop_toBottomOf="@id/tv_issuer" tools:text="48 hashes" /> - - - + app:layout_constraintTop_toBottomOf="@+id/tv_signed_hashes_title"> + + + + + + app:layout_constraintTop_toBottomOf="@id/ll_manage_security" /> + app:title="@string/details_manage_security_title" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_onboarding_main.xml b/app/src/main/res/layout/fragment_onboarding_main.xml index 6d567ce91b..82b94a5def 100644 --- a/app/src/main/res/layout/fragment_onboarding_main.xml +++ b/app/src/main/res/layout/fragment_onboarding_main.xml @@ -33,11 +33,12 @@ android:orientation="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + 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..904ab50059 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_send.xml b/app/src/main/res/layout/fragment_send.xml index 78131a2e15..1026392257 100644 --- a/app/src/main/res/layout/fragment_send.xml +++ b/app/src/main/res/layout/fragment_send.xml @@ -39,12 +39,14 @@ android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_currency_wallet.xml b/app/src/main/res/layout/item_currency_wallet.xml index 05af6fcb26..35b4a700af 100644 --- a/app/src/main/res/layout/item_currency_wallet.xml +++ b/app/src/main/res/layout/item_currency_wallet.xml @@ -20,7 +20,7 @@ android:layout_height="wrap_content" android:minHeight="76dp"> - + app:layout_constraintTop_toTopOf="@id/guideline" + tools:text="@string/lorem_ipsum" /> diff --git a/app/src/main/res/layout/item_popular_token.xml b/app/src/main/res/layout/item_popular_token.xml index 3f03f0279f..c3904baa73 100644 --- a/app/src/main/res/layout/item_popular_token.xml +++ b/app/src/main/res/layout/item_popular_token.xml @@ -8,7 +8,7 @@ android:background="@android:color/white" android:minHeight="60dp"> - - + - + - + 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..29a44311c8 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" /> + app:layout_constraintTop_toBottomOf="@id/tv_fiat_amount" + tools:text="@string/wallet_balance_verified" /> - - + app:layout_constraintTop_toBottomOf="@id/tv_fiat_amount" + tools:text="@string/lorem_ipsum" /> - - - + app:constraint_referenced_ids="tv_amount, tv_fiat_amount, tv_status" /> \ 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_send_fee.xml b/app/src/main/res/layout/layout_send_fee.xml index d1c30381f6..f54f8f8bfe 100644 --- a/app/src/main/res/layout/layout_send_fee.xml +++ b/app/src/main/res/layout/layout_send_fee.xml @@ -1,6 +1,7 @@ - + android:clipChildren="false"> - + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:translationY="-8dp" + app:selectionRequired="true" + app:singleLine="true" + app:singleSelection="true"> - + - + - + + + + - + 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_long_buttons.xml b/app/src/main/res/layout/layout_wallet_long_buttons.xml index d63cb4681a..fa2a403d60 100644 --- a/app/src/main/res/layout/layout_wallet_long_buttons.xml +++ b/app/src/main/res/layout/layout_wallet_long_buttons.xml @@ -27,7 +27,7 @@ android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintGuide_percent="0.37" /> + app:layout_constraintGuide_percent="0.5" /> - - + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_done_activation.xml b/app/src/main/res/layout/lp_onboarding_done_activation.xml index 1ed62c57f9..70be6417e0 100644 --- a/app/src/main/res/layout/lp_onboarding_done_activation.xml +++ b/app/src/main/res/layout/lp_onboarding_done_activation.xml @@ -12,6 +12,7 @@ android:visibility="gone" /> + + + +