diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index c8237d2ba3..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -FROM ubuntu:24.04 - -ENV DEBIAN_FRONTEND=noninteractive -ENV ANDROID_HOME=/opt/android-sdk -ENV BUNDLE_PATH=vendor/bundle -ENV PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools/34.0.0:$BUNDLE_PATH/bin -ENV LC_ALL=en_US.UTF-8 -ENV LANG=en_US.UTF-8 - -RUN apt-get update && apt-get install -y \ - openjdk-17-jdk \ - wget \ - unzip \ - curl \ - git \ - ruby \ - ruby-dev \ - build-essential \ - && apt-get clean - -RUN mkdir -p $ANDROID_HOME/cmdline-tools/latest && \ - wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O /tmp/cmdline-tools.zip && \ - unzip /tmp/cmdline-tools.zip -d $ANDROID_HOME/cmdline-tools/latest && \ - mv $ANDROID_HOME/cmdline-tools/latest/cmdline-tools/* $ANDROID_HOME/cmdline-tools/latest/ && \ - rm -rf $ANDROID_HOME/cmdline-tools/latest/cmdline-tools && \ - rm -f /tmp/cmdline-tools.zip && \ - yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses && \ - $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-31" "platforms;android-34" "build-tools;34.0.0" - -RUN wget https://github.com/lzhiyong/android-sdk-tools/releases/download/34.0.3/android-sdk-tools-static-aarch64.zip -O /tmp/android-sdk-tools-static-aarch64.zip && \ - unzip /tmp/android-sdk-tools-static-aarch64.zip -d /tmp/android-sdk-tools-static-arm && \ - cp -r /tmp/android-sdk-tools-static-arm/build-tools/* $ANDROID_HOME/build-tools/34.0.0/ && \ - rm -rf /tmp/android-sdk-tools-static-arm /tmp/android-sdk-tools-static-aarch64.zip - -RUN gem install bundler:2.5.23 -RUN gem install fastlane -v 2.225.0 -N -V -RUN gem install fastlane-plugin-firebase_app_distribution -v 0.9.1 -N -V - -COPY Gemfile Gemfile.lock ./ -RUN bundle install --jobs=4 --retry=3 --verbose - -RUN bundle exec fastlane -v - -CMD ["bash"] diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6cb76a20e4..7500382879 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,5 @@ import java.util.Properties +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants plugins { alias(deps.plugins.android.application) @@ -258,6 +259,10 @@ dependencies { implementation(deps.decompose.ext.compose) implementation(deps.moshi.adapters) + implementation(deps.moshi.kotlin) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) + /** Testing libraries */ testImplementation(deps.test.coroutine) testImplementation(deps.test.junit) @@ -275,6 +280,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) + debugPGImplementation(deps.chucker) mockedImplementation(deps.chuckerStub) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) @@ -296,5 +302,4 @@ dependencies { // excludes version 9999.0-empty-to-avoid-conflict-with-guava exclude(group = "com.google.guava", module = "listenablefuture") } - } \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2f9dc5a47e..2a7b205c9d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,21 +1,208 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html +-dontusemixedcaseclassnames +-flattenpackagehierarchy +-adaptclassstrings -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} +# firebase +-keep public class com.google.firebase.** { *; } +-keep class com.google.android.gms.internal.** { *; } +-keepclasseswithmembers class com.google.firebase.FirebaseException -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable +# hedera sdk +-keep class com.hedera.hashgraph.sdk.** { *; } +-keep interface com.hedera.hashgraph.sdk.** { *; } +-dontwarn com.esaulpaugh.headlong.** +-dontwarn edu.umd.cs.findbugs.annotations.SuppressFBWarnings +-dontwarn io.grpc.stub.** -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile +# trustwallet sdk +-keep class wallet.core.jni.** { *; } + +# solana +-keep class org.p2p.solanaj.** { *; } +-keep class com.tangem.blockchain.blockchains.solana.solanaj.model.** { *; } + +# binance +-keep class com.tangem.blockchain.blockchains.binance.client.** { *; } + +# shadow gson +-keep,allowobfuscation,allowshrinking class shadow.com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends shadow.com.google.gson.reflect.TypeToken + +# polkadot +-keep class io.emeraldpay.polkaj.api.RpcRequest { *; } +-keep class io.emeraldpay.polkaj.api.RpcResponse { *; } +-keep class io.emeraldpay.polkaj.api.RpcResponseError { *; } + +# kethereum +-keep class org.kethereum.bip32.model.ExtendedKey** +-keepclassmembers class org.kethereum.bip32.model.ExtendedKey** { *; } + +# some crypto +-dontwarn net.i2p.crypto.** +-dontwarn java.net.http.** +-dontwarn lombok.** + +-keep class org.spongycastle.** { *; } + +-dontwarn org.apache.hc.core5.** +-dontwarn org.apache.hc.client5.** +-dontwarn org.apache.log4j.config.** + +-dontwarn javax.naming.** +-dontwarn javax.xml.stream.** +-dontwarn javax.script.** + +-dontwarn org.java_websocket.client.WebSocketClient +-dontwarn org.java_websocket.handshake.ServerHandshake + +-dontwarn aQute.bnd.annotation.spi.ServiceProvider + +-dontwarn org.threeten.bp.** +-keep class org.threeten.bp.* +-keepclassmembers class org.threeten.bp.** { *; } + +# joda time +# These aren't necessary if including joda-convert +-dontwarn org.joda.convert.FromString +-dontwarn org.joda.convert.ToString + +-keepnames class org.joda.** implements java.io.Serializable +-keepclassmembers class org.joda.** implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} +# joda time + +# The name of @JsonClass types is used to look up the generated adapter. +-keepnames @com.squareup.moshi.JsonClass class * + +-keepclassmembers class * { + @com.squareup.moshi.FromJson ; + @com.squareup.moshi.ToJson ; +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class kotlin.Metadata { *; } +-keepclassmembers class kotlin.Metadata { + public ; +} + +# Proguard configuration for Jackson 2.x +-keep class com.fasterxml.** { *; } +-keep class com.fasterxml.jackson.databind.ObjectMapper { + public ; + protected ; +} +-keep class com.fasterxml.jackson.databind.ObjectWriter { + public ** writeValueAsString(**); +} +-keepnames class com.fasterxml.jackson.** { *; } +-dontwarn com.fasterxml.jackson.databind.** +-keep class * implements com.fasterxml.jackson.core.type.TypeReference + +-keep class kotlin.reflect.** +-keep public class kotlin.reflect.jvm.internal.impl.** { public *; } + +# Kotlin serialization looks up the generated serializer classes through a function on companion +# objects. The companions are looked up reflectively so we need to explicitly keep these functions. +-keepclasseswithmembers class **.*$Companion { + kotlinx.serialization.KSerializer serializer(...); +} +# If a companion has the serializer function, keep the companion field on the original type so that +# the reflective lookup succeeds. +-if class **.*$Companion { + kotlinx.serialization.KSerializer serializer(...); +} +-keepclassmembers class <1>.<2> { + <1>.<2>$Companion Companion; +} + +-keepnames class * implements android.os.Parcelable { + public static final ** CREATOR; +} +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} + +-dontwarn okhttp3.** +-dontwarn okio.** + +# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and +# EnclosingMethod is required to use InnerClasses. +-keepattributes Signature, InnerClasses, EnclosingMethod, *Annotation*, Exceptions + +# Retrofit does reflection on method and parameter annotations. +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations + +# Keep annotation default values (e.g., retrofit2.http.Field.encoded). +-keepattributes AnnotationDefault + +# Retain service method parameters when optimizing. +-keepclassmembers,allowshrinking,allowobfuscation interface * { + @retrofit2.http.* ; +} + +-keepclasseswithmembers class * { + @retrofit2.http.* ; +} +-keepclassmembernames interface * { + @retrofit.http.* ; +} + +# Ignore annotation used for build tooling. +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +# Ignore JSR 305 annotations for embedding nullability information. +-dontwarn javax.annotation.** + +# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. +-dontwarn kotlin.Unit + +# Top-level functions that can only be used by Kotlin. +-dontwarn retrofit2.KotlinExtensions +-dontwarn retrofit2.KotlinExtensions.* + +# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy +# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> + +# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). +-keep,allowobfuscation,allowshrinking interface retrofit2.Call +-keep,allowobfuscation,allowshrinking class retrofit2.Response + +# With R8 full mode generic signatures are stripped for classes that are not +# kept. Suspend functions are wrapped in continuations where the type argument +# is used. +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation + +# public tangem android sdk +-keep class com.tangem.operations.** { *; } +-keep class com.tangem.crypto.** { *; } +-keep class com.tangem.common.card.** { *; } + +# TODO remove (easy to fix in sdk) +-keep class com.tangem.common.json.** { *; } +-keep class com.tangem.common.SuccessResponse { *; } +-keep class com.tangem.common.UserCode { *; } +-keep class com.tangem.common.UserCodeType { *; } + +# non-sensitive enums +-keep enum com.tangem.domain.apptheme.model.AppThemeMode { *; } + +-keep class com.reown.walletkit.client.Wallet$Model { *; } +-keep class com.reown.walletkit.client.Wallet { *; } + +-keep class **.R +-keep class **.R$* { + ; +} diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 72d9d4b3c3..104b52520e 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 72d9d4b3c3f8e6604078798c4b5567327ed9b48a +Subproject commit 104b52520e8721eb775e2733deb361eed5067a63 diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index d54f1c4c78..c8408c6e91 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -222,7 +222,10 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private fun updateLogFiles() { appLogsStore.deleteOldLogsFile() - appLogsStore.deleteLastLogFile() + + if (!BuildConfig.TESTER_MENU_ENABLED) { + appLogsStore.deleteLastLogFile() + } // Temporally logs are not saved // scope.launch { diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt index 1b3f599f3f..a2011ff5e7 100644 --- a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt +++ b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt @@ -35,7 +35,10 @@ class TangemAppLoggerInitializer( } if (PERMITTED_PRIORITY.contains(priority)) { - appLogsStore.saveLogMessage(message) + appLogsStore.saveLogMessage( + tag = tag ?: "TangemAppLogger", + message = message, + ) } } } diff --git a/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt index 7e975e8f02..a6128ddfe6 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt @@ -33,22 +33,24 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor( private val tokensAdapter = moshi.adapter(VisaAuthTokens::class.java) - override suspend fun store(tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) { + override suspend fun store(cardId: String, tokens: VisaAuthTokens) = withContext(dispatcherProvider.io) { val json = tokensAdapter.toJson(tokens) secureStorage.store( json.encodeToByteArray(throwOnInvalidSequence = true), - VISA_AUTH_TOKENS_KEY, + createKey(cardId), ) } - override suspend fun get(): VisaAuthTokens? = withContext(dispatcherProvider.io) { - secureStorage.get(VISA_AUTH_TOKENS_KEY) + override suspend fun get(cardId: String): VisaAuthTokens? = withContext(dispatcherProvider.io) { + secureStorage.get(createKey(cardId)) ?.decodeToString(throwOnInvalidSequence = true) ?.let(tokensAdapter::fromJson) } - private companion object { - const val VISA_AUTH_TOKENS_KEY = "visa_auth_tokens" + override fun remove(cardId: String) { + secureStorage.delete(createKey(cardId)) } + + private fun createKey(cardId: String): String = "visa_auth_tokens_$cardId" } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt index a85c3e4667..f197705464 100644 --- a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt +++ b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt @@ -15,6 +15,6 @@ internal class TangemBlockchainSDKLogger( ) : BlockchainSDKLogger { override fun log(level: BlockchainSDKLogger.Level, message: String) { - appLogsStore.saveLogMessage(message) + appLogsStore.saveLogMessage(tag = "BlockchainSDK_${level.name}", message) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 55fb95de26..789f56e454 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -2,7 +2,6 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.* import com.tangem.domain.card.repository.CardRepository -import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase @@ -22,14 +21,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object CardDomainModule { - @Provides - @Singleton - fun provideSetAccessCodeRequestPolicyUseCase( - cardSdkConfigRepository: CardSdkConfigRepository, - ): SetAccessCodeRequestPolicyUseCase { - return SetAccessCodeRequestPolicyUseCase(cardSdkConfigRepository = cardSdkConfigRepository) - } - @Provides @Singleton fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase { diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index ca9f43d672..afc7a73f59 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.onramp.* +import com.tangem.domain.onramp.repositories.HotCryptoRepository import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository @@ -229,4 +230,10 @@ internal object OnrampDomainModule { ): FetchOnrampCountriesUseCase { return FetchOnrampCountriesUseCase(onrampRepository, onrampErrorResolver) } + + @Provides + @Singleton + fun provideGetHotCryptoTokensUseCase(hotCryptoRepository: HotCryptoRepository): GetHotCryptoUseCase { + return GetHotCryptoUseCase(hotCryptoRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt index 381de50f84..af52907af8 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/PromoDomainModule.kt @@ -19,12 +19,6 @@ internal object PromoDomainModule { return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository) } - @Provides - @Singleton - fun provideShouldShowRingPromoUseCase(promoRepository: PromoRepository): ShouldShowRingPromoUseCase { - return ShouldShowRingPromoUseCase(promoRepository) - } - @Provides @Singleton fun provideShouldShowSwapPromoTokenUseCase(promoRepository: PromoRepository): ShouldShowSwapPromoTokenUseCase { diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 6871b3101c..7dc5634128 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -182,12 +182,6 @@ internal object SettingsDomainModule { ): NeverRequestPermissionUseCase { return NeverRequestPermissionUseCase(repository = permissionRepository) } - - @Provides - @Singleton - fun provideShouldSaveAccessCodesUseCase(settingsRepository: SettingsRepository): ShouldSaveAccessCodesUseCase { - return ShouldSaveAccessCodesUseCase(settingsRepository = settingsRepository) - } // endregion @Provides diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 7ce6aa9702..4cda23bbe9 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -26,7 +26,7 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.VisaAuthChallenge -import com.tangem.domain.visa.model.VisaCardActivationResponse +import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.ScanTask @@ -35,16 +35,19 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.preflightread.PreflightReadFilter +import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.sdk.api.TangemSdkManager +import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.tap.derivationsFinder import com.tangem.tap.domain.tasks.product.CreateProductWalletTask import com.tangem.tap.domain.tasks.product.ResetBackupCardTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask +import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask import com.tangem.tap.domain.twins.FinalizeTwinTask @@ -494,6 +497,17 @@ internal class DefaultTangemSdkManager( } } + override suspend fun visaCustomerWalletApprove( + visaDataForApprove: VisaDataForApprove, + ): CompletionResult { + return runTaskAsyncReturnOnMain( + runnable = VisaCustomerWalletApproveTask( + visaDataForApprove = visaDataForApprove, + ), + initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), + ) + } + // endregion companion object { diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index c0dd2c2c03..b6291e7a12 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -19,13 +19,15 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.VisaAuthChallenge -import com.tangem.domain.visa.model.VisaCardActivationResponse +import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter +import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.sdk.api.TangemSdkManager +import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.tap.domain.sdk.mocks.MockProvider @Suppress("TooManyFunctions") @@ -207,4 +209,12 @@ class MockTangemSdkManager( ): CompletionResult { error("Not implemented") } + + override suspend fun visaCustomerWalletApprove( + visaDataForApprove: VisaDataForApprove, + ): CompletionResult { + error("Not implemented") + } + + // endregion } \ 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 bc2268c5a9..fdc3898fbf 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 @@ -311,15 +311,12 @@ private class ScanWalletProcessor( } private fun getWalletProductType(card: CardDTO): ProductType { - if (RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX)) { - return ProductType.Ring - } - return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && - card.settings.isKeysImportAllowed - ) { - ProductType.Wallet2 - } else { - ProductType.Wallet + return when { + card.isVisa -> ProductType.Visa + RING_BATCH_IDS.contains(card.batchId) || card.batchId.startsWith(RING_BATCH_PREFIX) -> ProductType.Ring + card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && + card.settings.isKeysImportAllowed -> ProductType.Wallet2 + else -> ProductType.Wallet } } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index 184e2a3f63..920c63ac0e 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -1,16 +1,20 @@ package com.tangem.tap.domain.tasks.visa -import com.reown.util.hexToBytes import com.tangem.common.CompletionResult import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.common.map +import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource +import com.tangem.crypto.CryptoUtils import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.datasource.local.visa.VisaOTPStorage +import com.tangem.datasource.local.visa.hasSavedOTP import com.tangem.domain.common.visa.VisaUtilities +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository @@ -20,12 +24,15 @@ import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.wallet.CreateWalletTask +import com.tangem.sdk.api.visa.VisaCardActivationResponse import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.* +import timber.log.Timber import kotlin.coroutines.resume import kotlin.jvm.Throws +import kotlin.time.measureTimedValue @Suppress("LongParameterList") class VisaCardActivationTask @AssistedInject constructor( @@ -36,9 +43,15 @@ class VisaCardActivationTask @AssistedInject constructor( private val otpStorage: VisaOTPStorage, private val visaAuthTokenStorage: VisaAuthTokenStorage, private val visaAuthRepository: VisaAuthRepository, - private val visaActivationRepository: VisaActivationRepository, + private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, ) : CardSessionRunnable { + private class SessionContext( + val visaActivationRepository: VisaActivationRepository, + val cardId: String, + val session: CardSession, + ) + override fun run(session: CardSession, callback: CompletionCallback) { coroutineScope.launch { callback(runSuspend(session)) @@ -52,33 +65,47 @@ class VisaCardActivationTask @AssistedInject constructor( return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.WrongCard.message)) } - return if (challengeToSign != null) { - signAuthorizationChallenge(session, challengeToSign) - } else { - val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() } - .getOrElse { - return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "")) - } + val visaActivationRepository = visaActivationRepositoryFactory.create(card.cardId) - signOrder(session, activationOrder) - } - } + val context = SessionContext( + visaActivationRepository = visaActivationRepository, + cardId = card.cardId, + session = session, + ) - private suspend fun signAuthorizationChallenge( - session: CardSession, - challengeToSign: VisaAuthChallenge.Card, - ): CompletionResult { - val attestationCommand = AttestCardKeyCommand(challenge = challengeToSign.challenge.hexToBytes()) - val result = suspendCancellableCoroutine { continuation -> - attestationCommand.run(session = session) { attestationResponse -> - continuation.resume(attestationResponse) + val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { + if (challengeToSign != null) { + context.signAuthorizationChallenge(challengeToSign) + } else { + val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() } + .getOrElse { + return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "")) + } + + context.signOrder(activationOrder) } } + Timber.i("VisaCardActivationTask all time: ${timedResult.duration}") + return timedResult.value + } - return when (result) { + private suspend fun SessionContext.signAuthorizationChallenge( + challengeToSign: VisaAuthChallenge.Card, + ): CompletionResult { + val attestationCommand = AttestCardKeyCommand(challenge = CryptoUtils.generateRandomBytes(length = 16)) + val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { + suspendCancellableCoroutine { continuation -> + attestationCommand.run(session = session) { attestationResponse -> + continuation.resume(attestationResponse) + } + } + } + Timber.i("AttestCardKeyCommand time: ${timedResult.duration}") + + return when (val result = timedResult.value) { is CompletionResult.Success -> { + Timber.i("AttestCardKeyCommand success") processSignedAuthorizationChallenge( - session = session, signedChallenge = challengeToSign.toSignedChallenge( signedChallenge = result.data.cardSignature.toHexString(), salt = result.data.salt.toHexString(), @@ -86,13 +113,13 @@ class VisaCardActivationTask @AssistedInject constructor( ) } is CompletionResult.Failure -> { + Timber.e("AttestCardKeyCommand failure ${result.error}") CompletionResult.Failure(result.error) } } } - private suspend fun processSignedAuthorizationChallenge( - session: CardSession, + private suspend fun SessionContext.processSignedAuthorizationChallenge( signedChallenge: VisaAuthSignedChallenge, ): CompletionResult { return coroutineScope { @@ -107,12 +134,14 @@ class VisaCardActivationTask @AssistedInject constructor( otpTaskDeferred.await() - signOrder(session, order) + signOrder(order) } } @Throws(TangemSdkError::class) - private suspend fun getActivationOrderToSign(signedChallenge: VisaAuthSignedChallenge): ActivationOrder { + private suspend fun SessionContext.getActivationOrderToSign( + signedChallenge: VisaAuthSignedChallenge, + ): ActivationOrder { val tokens = runCatching { visaAuthRepository.getAccessTokens(signedChallenge) }.getOrElse { @@ -121,12 +150,12 @@ class VisaCardActivationTask @AssistedInject constructor( ) } - visaAuthTokenStorage.store(tokens) + visaAuthTokenStorage.store(cardId, tokens) return visaActivationRepository.getActivationOrderToSign() } - private suspend fun createWallet(session: CardSession): CompletionResult { + private suspend fun SessionContext.createWallet(session: CardSession): CompletionResult { coroutineScope { ensureActive() } val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) @@ -135,122 +164,152 @@ class VisaCardActivationTask @AssistedInject constructor( createOTP(session) } else { val createWalletTask = CreateWalletTask(VisaUtilities.mandatoryCurve) - val result = suspendCancellableCoroutine { continuation -> - createWalletTask.run(session) { createWalletResult -> - continuation.resume(createWalletResult) + + val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { + suspendCancellableCoroutine { continuation -> + createWalletTask.run(session) { createWalletResult -> + continuation.resume(createWalletResult) + } } } - when (result) { + Timber.i("CreateWalletTask time: ${timedResult.duration}") + + when (val result = timedResult.value) { is CompletionResult.Success -> { + Timber.i("CreateWalletTask success") createOTP(session) } is CompletionResult.Failure -> { + Timber.e("CreateWalletTask failure ${result.error}") CompletionResult.Failure(result.error) } } } } - private suspend fun createOTP(session: CardSession): CompletionResult { + private suspend fun SessionContext.createOTP(session: CardSession): CompletionResult { coroutineScope { ensureActive() } - val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - - val otp = otpStorage.getOTP(card.cardId) - return if (otp != null) { + return if (otpStorage.hasSavedOTP(cardId)) { CompletionResult.Success(Unit) } else { val otpCommand = GenerateOTPCommand() - val result = suspendCancellableCoroutine { continuation -> - otpCommand.run(session) { otpResult -> - continuation.resume(otpResult) + val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { + suspendCancellableCoroutine { continuation -> + otpCommand.run(session) { otpResult -> + continuation.resume(otpResult) + } } } - when (result) { + Timber.i("GenerateOTPCommand time: ${timedResult.duration}") + + when (val result = timedResult.value) { is CompletionResult.Success -> { - otpStorage.saveOTP(card.cardId, result.data.rootOTP) + Timber.i("GenerateOTPCommand success") + otpStorage.saveOTP(cardId, result.data.rootOTP) CompletionResult.Success(Unit) } is CompletionResult.Failure -> { + Timber.e("GenerateOTPCommand failure ${result.error}") CompletionResult.Failure(result.error) } } } } - private suspend fun signOrder( - session: CardSession, - order: ActivationOrder, - ): CompletionResult { - val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + private suspend fun SessionContext.signOrder(order: ActivationOrder): CompletionResult { + val card = + session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) val wallet = - card.wallets.firstOrNull() ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - val task = SignHashCommand(order.hash.hexToBytes(), wallet.publicKey) - val result = suspendCancellableCoroutine { continuation -> - task.run(session) { signResult -> - continuation.resume(signResult) + card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } + ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + val task = SignHashCommand( + hash = order.hash.hexToBytes(), + walletPublicKey = wallet.publicKey, + derivationPath = VisaUtilities.visaDefaultDerivationPath, + ) + + val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { + suspendCancellableCoroutine { continuation -> + task.run(session) { signResult -> + continuation.resume(signResult) + } } } - return when (result) { + Timber.i("SignHashCommand time: ${timedResult.duration}") + + return when (val result = timedResult.value) { is CompletionResult.Success -> { + Timber.i("SignHashCommand success") handleSignedOrder( - session = session, activationOrder = order, response = result.data, ) } is CompletionResult.Failure -> { + Timber.e("SignHashCommand failure ${result.error}") CompletionResult.Failure(result.error) } } } - private suspend fun handleSignedOrder( - session: CardSession, + private suspend fun SessionContext.handleSignedOrder( activationOrder: ActivationOrder, response: SignHashResponse, ): CompletionResult { - val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - val signedOrder = SignedActivationOrder( activationOrder = activationOrder, signature = response.signature.toHexString(), ) - val otp = otpStorage.getOTP(card.cardId) ?: return CompletionResult.Failure( + val otp = otpStorage.getOTP(cardId) ?: return CompletionResult.Failure( TangemSdkError.Underlying(VisaActivationError.MissingRootOTP.message), ) - val activationResponse = VisaCardActivationResponse( - signedActivationOrder = signedOrder, - rootOTP = VisaRootOTP(otp.toHexString()), - ) + return setupAccessCode().map { + val card = + session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - return setupAccessCode(session).map { activationResponse } + VisaCardActivationResponse( + signedActivationOrder = signedOrder, + rootOTP = VisaRootOTP(otp.toHexString()), + newCardDTO = CardDTO(card), + ) + } } - private suspend fun setupAccessCode(session: CardSession): CompletionResult { + private suspend fun SessionContext.setupAccessCode(): CompletionResult { val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) if (card.isAccessCodeSet) { return CompletionResult.Success(Unit) } + Timber.i("Setting access code") + val task = SetUserCodeCommand.changeAccessCode(accessCode) - val result = suspendCancellableCoroutine { continuation -> - task.run(session) { setAccessCodeResult -> - continuation.resume(setAccessCodeResult) + + val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { + suspendCancellableCoroutine { continuation -> + task.run(session) { setAccessCodeResult -> + continuation.resume(setAccessCodeResult) + } } } - return when (result) { + Timber.i("SetUserCodeCommand time: ${timedResult.duration}") + + return when (val result = timedResult.value) { is CompletionResult.Success -> { + Timber.i("SetUserCodeCommand success") CompletionResult.Success(Unit) } is CompletionResult.Failure -> { + Timber.i("SetUserCodeCommand failure ${result.error}") CompletionResult.Failure(result.error) } } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt new file mode 100644 index 0000000000..541f367b08 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -0,0 +1,197 @@ +package com.tangem.tap.domain.tasks.visa + +import arrow.core.getOrElse +import com.tangem.common.CompletionResult +import com.tangem.common.card.Card +import com.tangem.common.card.CardWallet +import com.tangem.common.core.CardSession +import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.CompletionCallback +import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.hexToBytes +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.common.visa.VisaUtilities +import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility +import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.visa.model.VisaActivationError +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.operations.ScanTask +import com.tangem.operations.derivation.DeriveWalletPublicKeyTask +import com.tangem.operations.sign.SignHashCommand +import com.tangem.operations.sign.SignHashResponse + +class VisaCustomerWalletApproveTask( + private val visaDataForApprove: VisaDataForApprove, +) : CardSessionRunnable { + + override fun run(session: CardSession, callback: CompletionCallback) { + val card = session.environment.card ?: run { + callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) + return + } + + if (VisaUtilities.isVisaCard(card.firmwareVersion.doubleValue, card.batchId)) { + callback(CompletionResult.Failure(TangemSdkError.Underlying("Can't use Visa card for approve"))) + return + } + + if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) { + callback( + CompletionResult.Failure( + TangemSdkError.Underlying("Use tangem wallet specified during visa registration"), + ), + ) + return + } + + if (card.settings.isHDWalletAllowed) { + proceedApprove(card, session, callback) + } else { + proceedApproveWithLegacyCard(card, session, callback) + } + } + + private fun proceedApprove(card: Card, session: CardSession, callback: CompletionCallback) { + val cardDTO = CardDTO(card) + + val derivationStyle = cardDTO.derivationStyleProvider.getDerivationStyle() ?: run { + proceedApproveWithLegacyCard( + card = card, + session = session, + callback = callback, + ) + return + } + + val derivationPath = VisaUtilities.visaDefaultDerivationPath(derivationStyle) ?: run { + callback( + CompletionResult.Failure( + TangemSdkError.Underlying("Failed to generate derivation path with provided derivation style"), + ), + ) + return + } + + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run { + callback(CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.MissingWallet.message))) + return + } + + val derivationTask = DeriveWalletPublicKeyTask( + walletPublicKey = wallet.publicKey, + derivationPath = derivationPath, + ) + + derivationTask.run(session) { result -> + when (result) { + is CompletionResult.Success -> { + processDerivedKey( + wallet = wallet, + extendedPublicKey = result.data, + derivationPath = derivationPath, + session = session, + callback = callback, + ) + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + } + + private fun processDerivedKey( + wallet: CardWallet, + extendedPublicKey: ExtendedPublicKey, + derivationPath: DerivationPath, + session: CardSession, + callback: CompletionCallback, + ) { + val validationResult = VisaWalletPublicKeyUtility.validateExtendedPublicKey( + targetAddress = visaDataForApprove.targetAddress, + extendedPublicKey = extendedPublicKey, + ) + + validationResult.onLeft { + callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message))) + return + } + + signApproveData( + targetWalletPublicKey = wallet.publicKey, + derivationPath = derivationPath, + session = session, + callback = callback, + ) + } + + private fun proceedApproveWithLegacyCard( + card: Card, + session: CardSession, + callback: CompletionCallback, + ) { + val publicKey = findKeyWithoutDerivation( + targetAddress = visaDataForApprove.targetAddress, + card = CardDTO(card), + ).getOrElse { + callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message))) + return + } + + signApproveData( + targetWalletPublicKey = publicKey, + derivationPath = null, + session = session, + callback = callback, + ) + } + + private fun signApproveData( + targetWalletPublicKey: ByteArray, + derivationPath: DerivationPath?, + session: CardSession, + callback: CompletionCallback, + ) { + val signTask = SignHashCommand( + hash = visaDataForApprove.approveHash.hexToBytes(), + walletPublicKey = targetWalletPublicKey, + derivationPath = derivationPath, + ) + + signTask.run(session) { result -> + when (result) { + is CompletionResult.Success -> { + scanCard( + signHashResponse = result.data, + session = session, + callback = callback, + ) + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + } + + private fun scanCard( + signHashResponse: SignHashResponse, + session: CardSession, + callback: CompletionCallback, + ) { + val scanTask = ScanTask() + scanTask.run(session) { result -> + when (result) { + is CompletionResult.Success -> { + callback(CompletionResult.Success(signHashResponse)) + } + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt index 8a3f2fe559..2697b453b7 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.userWalletList.model +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -7,16 +8,24 @@ import com.tangem.domain.wallets.models.UserWalletId @JsonClass(generateAdapter = true) internal data class UserWalletSensitiveInformation( + @Json(name = "wallets") val wallets: List, ) @JsonClass(generateAdapter = true) internal data class UserWalletPublicInformation( + @Json(name = "name") val name: String, + @Json(name = "walletId") val walletId: UserWalletId, + @Json(name = "artworkUrl") val artworkUrl: String, + @Json(name = "cardsInWallet") val cardsInWallet: Set, + @Json(name = "scanResponse") val scanResponse: ScanResponse, + @Json(name = "isMultiCurrency") val isMultiCurrency: Boolean, + @Json(name = "hasBackupError") val hasBackupError: Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index c231b0ba49..42e9b20d4a 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.card.CardWallet import com.tangem.common.core.CardSession import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey @@ -24,10 +25,16 @@ import kotlin.coroutines.resume internal class VisaCardScanHandler @Inject constructor( private val visaAuthRepository: VisaAuthRepository, - private val visaActivationRepository: VisaActivationRepository, + private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, private val visaAuthTokenStorage: VisaAuthTokenStorage, ) { + private class SessionContext( + val visaActivationRepository: VisaActivationRepository, + val cardId: String, + val session: CardSession, + ) + suspend fun handleVisaCardScan(session: CardSession): CompletionResult { Timber.i("Attempting to handle Visa card scan") @@ -36,6 +43,14 @@ internal class VisaCardScanHandler @Inject constructor( return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) } + val visaActivationRepository = visaActivationRepositoryFactory.create(card.cardId) + + val context = SessionContext( + visaActivationRepository = visaActivationRepository, + cardId = card.cardId, + session = session, + ) + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run { val activationInput = VisaActivationInput(card.cardId, card.cardPublicKey, card.isAccessCodeSet) @@ -43,13 +58,10 @@ internal class VisaCardScanHandler @Inject constructor( return CompletionResult.Success(activationStatus) } - return deriveKey(wallet, session) + return context.deriveKey(wallet) } - private suspend fun deriveKey( - wallet: CardWallet, - session: CardSession, - ): CompletionResult { + private suspend fun SessionContext.deriveKey(wallet: CardWallet): CompletionResult { val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { Timber.e("Failed to create derivation path while first scan") @@ -64,17 +76,16 @@ internal class VisaCardScanHandler @Inject constructor( continuation.resume(result) } } - return handleDerivationResponse(derivationTaskResult, session) + return handleDerivationResponse(derivationTaskResult) } - private suspend fun handleDerivationResponse( + private suspend fun SessionContext.handleDerivationResponse( result: CompletionResult, - session: CardSession, ): CompletionResult { return when (result) { is CompletionResult.Success -> { Timber.i("Start task for loading challenge for Visa wallet") - handleWalletAuthorization(session) + handleWalletAuthorization() } is CompletionResult.Failure -> { CompletionResult.Failure(result.error) @@ -82,9 +93,8 @@ internal class VisaCardScanHandler @Inject constructor( } } - private suspend fun handleWalletAuthorization(session: CardSession): CompletionResult { + private suspend fun SessionContext.handleWalletAuthorization(): CompletionResult { Timber.i("Started handling authorization using Visa wallet") - val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { Timber.e("Failed to create derivation path while handling wallet authorization") @@ -93,6 +103,8 @@ internal class VisaCardScanHandler @Inject constructor( ) } + val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: run { Timber.e("Failed to find extended public key while handling wallet authorization") return CompletionResult.Failure( @@ -108,7 +120,6 @@ internal class VisaCardScanHandler @Inject constructor( } Timber.i("Requesting challenge for wallet authorization") - // Will be changed later after backend implementation val challengeResponse = runCatching { visaAuthRepository.getCustomerWalletAuthChallenge( cardId = card.cardId, @@ -122,14 +133,12 @@ internal class VisaCardScanHandler @Inject constructor( publicKey = wallet.publicKey, derivationPath = derivationPath, nonce = challengeResponse.challenge, - session = session, ) return when (signChallengeResult) { is CompletionResult.Success -> { Timber.i("Challenge signed with Wallet public key") handleWalletAuthorizationTokens( - session = session, signedChallenge = challengeResponse .toSignedChallenge(signChallengeResult.data.signature.toHexString()), ) @@ -141,8 +150,7 @@ internal class VisaCardScanHandler @Inject constructor( } } - private suspend fun handleWalletAuthorizationTokens( - session: CardSession, + private suspend fun SessionContext.handleWalletAuthorizationTokens( signedChallenge: VisaAuthSignedChallenge, ): CompletionResult { val authorizationTokensResponse = runCatching { @@ -151,17 +159,20 @@ internal class VisaCardScanHandler @Inject constructor( Timber.i( "Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key", ) - return handleCardAuthorization(session) + return handleCardAuthorization() } - visaAuthTokenStorage.store(authorizationTokensResponse) + visaAuthTokenStorage.store( + cardId = cardId, + tokens = authorizationTokensResponse, + ) Timber.i("Authorized using Wallet public key successfully") return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse)) } - private suspend fun handleCardAuthorization(session: CardSession): CompletionResult { + private suspend fun SessionContext.handleCardAuthorization(): CompletionResult { val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) Timber.i("Requesting authorization challenge to sign") @@ -178,7 +189,7 @@ internal class VisaCardScanHandler @Inject constructor( Timber.i("Received challenge to sign: ${challengeResponse.challenge}") - val signChallengeResult = signChallengeWithCard(session = session, challenge = challengeResponse.challenge) + val signChallengeResult = signChallengeWithCard(challenge = challengeResponse.challenge) val attestCardKeyResponse = when (signChallengeResult) { is CompletionResult.Success -> { @@ -210,7 +221,10 @@ internal class VisaCardScanHandler @Inject constructor( ) } - visaAuthTokenStorage.store(authorizationTokensResponse) + visaAuthTokenStorage.store( + cardId = card.cardId, + tokens = authorizationTokensResponse, + ) val activationRemoteState = visaActivationRepository.getActivationRemoteState() @@ -239,47 +253,31 @@ internal class VisaCardScanHandler @Inject constructor( ) } - private suspend fun signChallengeWithWallet( + private suspend fun SessionContext.signChallengeWithWallet( publicKey: ByteArray, derivationPath: DerivationPath, nonce: String, - session: CardSession, ): CompletionResult { - val signHashCommand = SignHashCommand(publicKey, nonce.toByteArray(), derivationPath) - val result = suspendCancellableCoroutine { + val signHashCommand = SignHashCommand( + hash = nonce.hexToBytes(), + walletPublicKey = publicKey, + derivationPath = derivationPath, + ) + return suspendCancellableCoroutine { signHashCommand.run(session) { result -> it.resume(result) } } - - return when (result) { - is CompletionResult.Success -> { - CompletionResult.Success(result.data) - } - is CompletionResult.Failure -> { - CompletionResult.Failure(result.error) - } - } } - private suspend fun signChallengeWithCard( - session: CardSession, + private suspend fun SessionContext.signChallengeWithCard( challenge: String, ): CompletionResult { - val signHashCommand = AttestCardKeyCommand(challenge = challenge.toByteArray()) - val result = suspendCancellableCoroutine { continuation -> + val signHashCommand = AttestCardKeyCommand(challenge = challenge.hexToBytes()) + return suspendCancellableCoroutine { continuation -> signHashCommand.run(session) { result -> continuation.resume(result) } } - - return when (result) { - is CompletionResult.Success -> { - CompletionResult.Success(result.data) - } - is CompletionResult.Failure -> { - CompletionResult.Failure(result.error) - } - } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt index 7c937b3772..f405179561 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt @@ -52,6 +52,8 @@ data class WcSignMessage( @Json(name = "type") val type: WCSignType, ) : WcRequestData { + + @JsonClass(generateAdapter = false) enum class WCSignType { MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_MESSAGE, } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Account.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Account.kt index 9372666b46..48114a57c4 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Account.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Account.kt @@ -1,3 +1,16 @@ package com.tangem.tap.domain.walletconnect2.domain.models -data class Account(val chainId: String, val walletAddress: String, val derivationPath: String?) \ No newline at end of file +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Account( + @Json(name = "chainId") + val chainId: String, + + @Json(name = "walletAddress") + val walletAddress: String, + + @Json(name = "derivationPath") + val derivationPath: String?, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Session.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Session.kt index 896b6dc97a..6950d29486 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Session.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Session.kt @@ -1,6 +1,13 @@ package com.tangem.tap.domain.walletconnect2.domain.models +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) data class Session( + @Json(name = "topic") val topic: String, + + @Json(name = "accounts") val accounts: List, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceCancelOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceCancelOrder.kt index aebac1b973..112c92e7a6 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceCancelOrder.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceCancelOrder.kt @@ -2,29 +2,34 @@ package com.tangem.tap.domain.walletconnect2.domain.models.binance import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.tap.domain.walletconnect2.domain.WcRequestData @Suppress("LongParameterList") @JsonClass(generateAdapter = true) -class WcBinanceCancelOrder( +data class WcBinanceCancelOrder( @Json(name = "account_number") - accountNumber: String, + val accountNumber: String, @Json(name = "chain_id") - chainId: String, + val chainId: String, @Json(name = "data") - data: String?, + val data: String?, @Json(name = "memo") - memo: String?, + val memo: String?, @Json(name = "sequence") - sequence: String, + val sequence: String, @Json(name = "source") - source: String, + val source: String, @Json(name = "msgs") - msgs: List, -) : WcBinanceOrder(accountNumber, chainId, data, memo, sequence, source, msgs) { + val msgs: List, +) : WcRequestData { + @JsonClass(generateAdapter = true) data class Message( + @Json(name = "refid") val refid: String, + @Json(name = "sender") val sender: String, + @Json(name = "symbol") val symbol: String, ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt deleted file mode 100644 index 1e3f33db29..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.tap.domain.walletconnect2.domain.models.binance - -import com.tangem.tap.domain.walletconnect2.domain.WcRequestData - -@Suppress("LongParameterList") -open class WcBinanceOrder( - val accountNumber: String, - val chainId: String, - val data: String?, - val memo: String?, - val sequence: String, - val source: String, - val msgs: List, -) : WcRequestData - -data class WcBinanceTxConfirmParam( - val ok: Boolean, - val errorMsg: String?, -) : WcRequestData \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradeOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradeOrder.kt index d00997295d..bdeaba5b27 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradeOrder.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradeOrder.kt @@ -4,21 +4,28 @@ import com.github.salomonbrys.kotson.jsonSerializer import com.google.gson.JsonObject import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.tap.domain.walletconnect2.domain.WcRequestData @Suppress("LongParameterList") @JsonClass(generateAdapter = true) -class WcBinanceTradeOrder( +data class WcBinanceTradeOrder( @Json(name = "account_number") - accountNumber: String, + val accountNumber: String, @Json(name = "chain_id") - chainId: String, - data: String?, - memo: String?, - sequence: String, - source: String, - msgs: List, -) : WcBinanceOrder(accountNumber, chainId, data, memo, sequence, source, msgs) { + val chainId: String, + @Json(name = "data") + val data: String?, + @Json(name = "memo") + val memo: String?, + @Json(name = "sequence") + val sequence: String, + @Json(name = "source") + val source: String, + @Json(name = "msgs") + val msgs: List, +) : WcRequestData { + @JsonClass(generateAdapter = false) enum class MessageKey(val key: String) { ID("id"), ORDER_TYPE("ordertype"), @@ -30,14 +37,23 @@ class WcBinanceTradeOrder( TIME_INFORCE("timeinforce"), } + @JsonClass(generateAdapter = true) data class Message( + @Json(name = "id") val id: String, + @Json(name = "orderType") val orderType: Int, + @Json(name = "price") val price: Long, + @Json(name = "quantity") val quantity: Long, + @Json(name = "sender") val sender: String, + @Json(name = "side") val side: Int, + @Json(name = "symbol") val symbol: String, + @Json(name = "timeInforce") val timeInforce: Int, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTransferOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTransferOrder.kt index 6d46e17319..99199c17dc 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTransferOrder.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTransferOrder.kt @@ -2,33 +2,48 @@ package com.tangem.tap.domain.walletconnect2.domain.models.binance import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.tap.domain.walletconnect2.domain.WcRequestData @Suppress("LongParameterList") @JsonClass(generateAdapter = true) -class WcBinanceTransferOrder( +data class WcBinanceTransferOrder( @Json(name = "account_number") - accountNumber: String, + val accountNumber: String, @Json(name = "chain_id") - chainId: String, - data: String?, - memo: String?, - sequence: String, - source: String, - msgs: List, -) : WcBinanceOrder(accountNumber, chainId, data, memo, sequence, source, msgs) { + val chainId: String, + @Json(name = "data") + val data: String?, + @Json(name = "memo") + val memo: String?, + @Json(name = "sequence") + val sequence: String, + @Json(name = "source") + val source: String, + @Json(name = "msgs") + val msgs: List, +) : WcRequestData { + @JsonClass(generateAdapter = true) data class Message( + @Json(name = "inputs") val inputs: List, + @Json(name = "outputs") val outputs: List, ) { + @JsonClass(generateAdapter = true) data class Item( + @Json(name = "address") val address: String, + @Json(name = "coins") val coins: List, ) { + @JsonClass(generateAdapter = true) data class Coin( + @Json(name = "amount") val amount: Long, + @Json(name = "denom") val denom: String, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTxConfirmParam.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTxConfirmParam.kt new file mode 100644 index 0000000000..24ada4ca98 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTxConfirmParam.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.domain.walletconnect2.domain.models.binance + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.tap.domain.walletconnect2.domain.WcRequestData + +@JsonClass(generateAdapter = true) +data class WcBinanceTxConfirmParam( + @Json(name = "ok") + val ok: Boolean, + @Json(name = "errorMsg") + val errorMsg: String?, +) : WcRequestData \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index bbf156324b..68d354a90c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -4,6 +4,7 @@ import android.os.Bundle import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute @@ -11,13 +12,16 @@ import com.tangem.common.routing.AppRouter import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.Analytics import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.getBackupCardsCount import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings @@ -34,17 +38,24 @@ import com.tangem.wallet.R import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @HiltViewModel internal class CardSettingsViewModel @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val tangemSdkManager: TangemSdkManager, private val cardSettingsInteractor: CardSettingsInteractor, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { + private var previousBiometricsRequestPolicy: Boolean = false + private val userWalletId = savedStateHandle.get(AppRoute.CardSettings.USER_WALLET_ID_KEY) ?.unbundle(UserWalletId.serializer()) ?: error("User wallet ID is required for CardSettingsViewModel") @@ -52,12 +63,32 @@ internal class CardSettingsViewModel @Inject constructor( val screenState: MutableStateFlow = MutableStateFlow(getInitialState()) init { + updateAccessCodeRequestPolicy() + cardSettingsInteractor.scannedScanResponse .filterNotNull() .onEach(::updateCardDetails) .launchIn(viewModelScope) } + override fun onCleared() { + // Restore the previous value of access code request policy + cardSdkConfigRepository.isBiometricsRequestPolicy = previousBiometricsRequestPolicy + } + + private fun updateAccessCodeRequestPolicy() { + runBlocking { + // !!!IMPORTANT!!!: Do not forget to restore the previous value in onCleared() method + previousBiometricsRequestPolicy = cardSdkConfigRepository.isBiometricsRequestPolicy + + val userWallet = getUserWalletUseCase(userWalletId) + .getOrElse { error("User wallet $userWalletId not found") } + + cardSdkConfigRepository.isBiometricsRequestPolicy = userWallet.scanResponse.card.isAccessCodeSet && + settingsRepository.shouldSaveAccessCodes() + } + } + private fun getInitialState() = CardSettingsScreenState( cardDetails = null, onElementClick = ::handleClickingItem, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt index 91558d9f57..5646eee001 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt @@ -4,7 +4,7 @@ import androidx.activity.compose.BackHandler import androidx.annotation.StringRes import androidx.compose.foundation.layout.* import androidx.compose.foundation.selection.selectable -import androidx.compose.material.* +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -27,13 +27,12 @@ internal fun SettingsScreensScaffold( addBottomInsets: Boolean = true, fab: @Composable () -> Unit = {}, ) { - val state = rememberScaffoldState(snackbarHostState = snackbarHostState) val backgroundColor = TangemTheme.colors.background.secondary BackHandler(onBack = onBackClick) Scaffold( - scaffoldState = state, + snackbarHost = { SnackbarHost(snackbarHostState) }, topBar = { EmptyTopBarWithNavigation( modifier = Modifier.statusBarsPadding(), @@ -43,7 +42,7 @@ internal fun SettingsScreensScaffold( }, modifier = modifier, contentWindowInsets = WindowInsetsZero, - backgroundColor = backgroundColor, + containerColor = backgroundColor, floatingActionButton = { Box(modifier = Modifier.navigationBarsPadding()) { fab() @@ -89,6 +88,7 @@ internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun EmptyTopBarWithNavigation( onBackClick: () -> Unit, @@ -108,8 +108,9 @@ internal fun EmptyTopBarWithNavigation( ) } }, - backgroundColor = backgroundColor, - elevation = 0.dp, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = backgroundColor, + ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt index b25fe77799..252fc23b51 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt @@ -5,7 +5,10 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -15,6 +18,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.analytics.Analytics +import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.analytics.events.Settings @@ -71,7 +75,7 @@ private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifie @Composable private fun EmptyScreen(state: WalletConnectScreenState) { if (state.isLoading) { - LinearProgressIndicator( + TangemLinearProgressIndicator( modifier = Modifier.fillMaxWidth(), color = TangemTheme.colors.icon.accent, ) @@ -102,7 +106,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) { @Composable private fun WalletConnectSessions(state: WalletConnectScreenState) { if (state.isLoading) { - LinearProgressIndicator( + TangemLinearProgressIndicator( modifier = Modifier .fillMaxWidth() .height(2.dp), diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt index 0df4db4753..9827fc5dbe 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt @@ -12,10 +12,10 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.settings.ShouldSaveAccessCodesUseCase +import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.tokens.TokensAction import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase @@ -39,11 +39,11 @@ import javax.inject.Inject @Suppress("LongParameterList") @HiltViewModel internal class HomeViewModel @Inject constructor( - private val shouldSaveAccessCodesUseCase: ShouldSaveAccessCodesUseCase, - private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, private val scanCardProcessor: ScanCardProcessor, private val generateWalletNameUseCase: GenerateWalletNameUseCase, private val saveWalletUseCase: SaveWalletUseCase, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, ) : ViewModel() { @@ -73,7 +73,7 @@ internal class HomeViewModel @Inject constructor( private fun scanCard() { viewModelScope.launch { - setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = shouldSaveAccessCodesUseCase()) + cardSdkConfigRepository.isBiometricsRequestPolicy = settingsRepository.shouldSaveAccessCodes() scanCardProcessor.scan( analyticsSource = AnalyticsParam.ScreensSources.Intro, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index b1940958e7..a1605112fc 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -13,6 +13,7 @@ import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet @@ -42,6 +43,12 @@ object OnboardingHelper { store.state.globalState.onboardingState.onboardingManager ?: OnboardingManager(response) val cardId = response.card.cardId return when { + response.cardTypesResolver.isVisaWallet() -> { + if (response.visaCardActivationStatus == null) error("Visa card activation status is null") + + response.visaCardActivationStatus !is VisaCardActivationStatus.Activated + } + response.cardTypesResolver.isTangemTwins() -> { if (!response.twinsIsTwinned()) { true @@ -66,7 +73,8 @@ object OnboardingHelper { fun whereToNavigate(scanResponse: ScanResponse): AppRoute { val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 || scanResponse.productType == ProductType.Ring || - scanResponse.productType == ProductType.Wallet // AppRoute.OnboardingOther is also supported + scanResponse.productType == ProductType.Wallet || + scanResponse.productType == ProductType.Visa // AppRoute.OnboardingOther is also supported if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled && newOnboardingSupportTypes ) { diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt index d0d7c64fb5..7062719124 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt @@ -4,8 +4,8 @@ import android.content.DialogInterface import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.SnackbarHost -import androidx.compose.material.SnackbarHostState +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index c34497d9f0..968b3776fa 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -6,8 +6,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material.SnackbarHost -import androidx.compose.material.SnackbarHostState +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt index 8b90cbbdc9..ee1a00bb48 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt @@ -7,8 +7,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material.SnackbarHost -import androidx.compose.material.SnackbarHostState +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt index 6685813017..921fce0ccc 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultVisaAuthProvider.kt @@ -1,14 +1,20 @@ package com.tangem.tap.network.auth import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider -import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.wallets.legacy.UserWalletsListManager import javax.inject.Inject internal class DefaultVisaAuthProvider @Inject constructor( - private val authStorage: VisaAuthTokenStorage, + private val userWalletsListManager: UserWalletsListManager, ) : TangemVisaAuthProvider { - override suspend fun getAuthHeader(): String { - return authStorage.get()?.accessToken?.let { "Bearer $it" } ?: "Error in the app!" + override suspend fun getAuthHeader(cardId: String): String { + val card = userWalletsListManager.userWalletsSync.firstOrNull { it.cardId == cardId } + val status = card?.scanResponse?.visaCardActivationStatus as? VisaCardActivationStatus.Activated + ?: return "Error in the app!" + val accessToken = status.visaAuthTokens.accessToken + + return "Bearer $accessToken" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt index 95f2c90b4c..6709842bb5 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt @@ -1,6 +1,7 @@ package com.tangem.tap.network.exchangeServices.mercuryo import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import retrofit2.http.GET import retrofit2.http.Path @@ -11,21 +12,25 @@ interface MercuryoApi { suspend fun currencies(@Path("apiVersion") apiVersion: String): MercuryoCurrenciesResponse } +@JsonClass(generateAdapter = true) data class MercuryoCurrenciesResponse( - val status: Int, - val data: Data, + @Json(name = "status") val status: Int, + @Json(name = "data") val data: Data, ) { + @JsonClass(generateAdapter = true) data class Data( - val fiat: List, - val crypto: List, - val config: Config, + @Json(name = "fiat") val fiat: List, + @Json(name = "crypto") val crypto: List, + @Json(name = "config") val config: Config, ) + @JsonClass(generateAdapter = true) data class Config( @Json(name = "crypto_currencies") val cryptoCurrencies: List, ) + @JsonClass(generateAdapter = true) data class MercuryoCryptoCurrency( @Json(name = "currency") val currencySymbol: String, diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt index 0d72c8158a..c177292cea 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt @@ -149,5 +149,7 @@ internal val Blockchain.mercuryoNetwork: String? Blockchain.Clore -> null Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet -> null Blockchain.Bitrock, Blockchain.BitrockTestnet -> null + Blockchain.Sonic, Blockchain.SonicTestnet -> null + Blockchain.ApeChain, Blockchain.ApeChainTestnet -> null } } \ 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 index e5948ce873..c8235a8f74 100644 --- 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 @@ -22,7 +22,9 @@ interface MoonPayApi { @JsonClass(generateAdapter = true) data class MoonPayUserStatus( + @Json(name = "isBuyAllowed") val isBuyAllowed: Boolean, + @Json(name = "isSellAllowed") val isSellAllowed: Boolean, @Json(name = "isAllowed") val isMoonpayAllowed: Boolean, @@ -34,18 +36,18 @@ data class MoonPayUserStatus( @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(), - val metadata: MoonPayCurrenciesMetadata? = null, + @Json(name = "type") val type: String, + @Json(name = "code") val code: String, + @Json(name = "supportsLiveMode") val supportsLiveMode: Boolean = false, + @Json(name = "isSuspended") val isSuspended: Boolean = true, + @Json(name = "isSupportedInUS") val isSupportedInUS: Boolean = false, + @Json(name = "isSellSupported") val isSellSupported: Boolean = false, + @Json(name = "notAllowedUSStates") val notAllowedUSStates: List = emptyList(), + @Json(name = "metadata") val metadata: MoonPayCurrenciesMetadata? = null, ) @JsonClass(generateAdapter = true) data class MoonPayCurrenciesMetadata( - val contractAddress: String?, - val networkCode: String?, + @Json(name = "contractAddress") val contractAddress: String?, + @Json(name = "networkCode") val networkCode: String?, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt index 729a87b31f..a3d9000b47 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt @@ -150,4 +150,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? VanarChainTestnet -> null OdysseyChain, OdysseyChainTestnet -> null Bitrock, BitrockTestnet -> null + Sonic, SonicTestnet -> null + ApeChain, ApeChainTestnet -> null } \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_rounded_shape.xml b/app/src/main/res/drawable/progress_rounded_shape.xml new file mode 100644 index 0000000000..7be741d331 --- /dev/null +++ b/app/src/main/res/drawable/progress_rounded_shape.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_onboarding_progress.xml b/app/src/main/res/layout/view_onboarding_progress.xml index 696a282eda..83dcb3354b 100644 --- a/app/src/main/res/layout/view_onboarding_progress.xml +++ b/app/src/main/res/layout/view_onboarding_progress.xml @@ -4,9 +4,10 @@ android:id="@+id/pb_state" style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="20dp" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:progressBackgroundTint="@color/icon_informative" - android:progressTint="@color/icon_primary_1" /> + android:paddingVertical="8dp" + android:indeterminate="false" + android:progressDrawable="@drawable/progress_rounded_shape" /> diff --git a/common/src/main/kotlin/com/tangem/common/timemeasure/RealtimeMonotonicTimeSource.kt b/common/src/main/kotlin/com/tangem/common/timemeasure/RealtimeMonotonicTimeSource.kt new file mode 100644 index 0000000000..0c2858265c --- /dev/null +++ b/common/src/main/kotlin/com/tangem/common/timemeasure/RealtimeMonotonicTimeSource.kt @@ -0,0 +1,9 @@ +package com.tangem.common.timemeasure + +import android.os.SystemClock +import kotlin.time.AbstractLongTimeSource +import kotlin.time.DurationUnit + +object RealtimeMonotonicTimeSource : AbstractLongTimeSource(DurationUnit.NANOSECONDS) { + override fun read(): Long = SystemClock.elapsedRealtimeNanos() +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/ChartMarker.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/ChartMarker.kt index 0005e47d15..5ef509c92b 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/ChartMarker.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/ChartMarker.kt @@ -15,7 +15,6 @@ import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerValueForma import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker import com.patrykandpatrick.vico.core.common.Dimensions import com.patrykandpatrick.vico.core.common.LayeredComponent -import com.patrykandpatrick.vico.core.common.component.Component import com.patrykandpatrick.vico.core.common.component.TextComponent import com.patrykandpatrick.vico.core.common.shape.Shape import com.tangem.core.ui.res.TangemTheme @@ -39,7 +38,28 @@ internal fun rememberTangemChartMarker(color: Color): CartesianMarker { object : DefaultCartesianMarker( label = TextComponent(textSizeSp = 0f), - indicator = ::indicator, + indicator = { color -> + val composeColor = Color(color) + + LayeredComponent( + rear = shapeComponent( + color = composeColor.copy(alpha = INDICATOR_REAR_COLOR_ALPHA), + shape = Shape.Pill, + ), + front = LayeredComponent( + rear = shapeComponent( + color = composeColor, + shape = Shape.Pill, + ), + front = shapeComponent( + color = Color.White, + shape = Shape.Pill, + ), + padding = indicatorPadding, + ), + padding = indicatorPadding, + ) + }, indicatorSizeDp = INDICATOR_SIZE_DP, guideline = guideline, valueFormatter = object : CartesianMarkerValueFormatter { @@ -81,29 +101,6 @@ internal fun rememberTangemChartMarker(color: Color): CartesianMarker { } } -private fun indicator(color: Int): Component { - val composeColor = Color(color) - - return LayeredComponent( - rear = shapeComponent( - color = composeColor.copy(alpha = INDICATOR_REAR_COLOR_ALPHA), - shape = Shape.Pill, - ), - front = LayeredComponent( - rear = shapeComponent( - color = composeColor, - shape = Shape.Pill, - ), - front = shapeComponent( - color = Color.White, - shape = Shape.Pill, - ), - padding = indicatorPadding, - ), - padding = indicatorPadding, - ) -} - private val indicatorPadding = Dimensions.of(3.dp) private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressHideButton.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressHideButton.kt new file mode 100644 index 0000000000..16a1484d52 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressHideButton.kt @@ -0,0 +1,45 @@ +package com.tangem.common.ui.expressStatus + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun ColumnScope.ExpressHideButton(isTerminal: Boolean, isAutoDisposable: Boolean, onClick: () -> Unit) { + AnimatedVisibility( + visible = isTerminal && !isAutoDisposable, + label = "Hide button visibility animation", + ) { + Text( + text = stringResourceSafe(R.string.express_status_hide_button_text), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.button, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 12.dp, top = 14.dp) + .clip(RoundedCornerShape(12.dp)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + onClick = onClick, + ) + .padding(vertical = 10.dp), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/OnrampStatusBottomSheetContent.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/OnrampStatusBottomSheetContent.kt index 35905de8f7..fc888d6c64 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/OnrampStatusBottomSheetContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/OnrampStatusBottomSheetContent.kt @@ -59,6 +59,11 @@ fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) { SpacerH12() ExpressStatusBlock(state = state.info.status) ExpressStatusNotificationBlock(state = state.info.notification) + ExpressHideButton( + isTerminal = state.activeStatus.isTerminal, + isAutoDisposable = state.activeStatus.isAutoDisposable, + onClick = state.info.onDisposeExpressStatus, + ) SpacerH24() } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionStateUM.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionStateUM.kt index 45138854f8..fb2489e331 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionStateUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionStateUM.kt @@ -30,6 +30,7 @@ data class ExpressTransactionStateInfoUM( val timestampFormatted: TextReference, val onGoToProviderClick: (String) -> Unit, val onClick: () -> Unit, + val onDisposeExpressStatus: () -> Unit, val iconState: ExpressTransactionStateIconUM, val toAmount: TextReference, val toFiatAmount: TextReference?, diff --git a/core/config-toggles/build.gradle.kts b/core/config-toggles/build.gradle.kts index 0a7bc83fdc..97534f26fa 100644 --- a/core/config-toggles/build.gradle.kts +++ b/core/config-toggles/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -11,7 +13,6 @@ android { } dependencies { - /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) @@ -23,6 +24,7 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.timber) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) /** Core modules */ implementation(projects.core.datasource) diff --git a/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json b/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json index aee72fb6e6..64770f0109 100644 --- a/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json +++ b/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json @@ -14,5 +14,13 @@ { "name": "vanar-chain", "version": "undefined" + }, + { + "name": "sonic", + "version": "undefined" + }, + { + "name": "apechain", + "version": "undefined" } ] \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 6592384f45..463e2e5f2d 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -21,7 +21,7 @@ }, { "name": "ONBOARDING_CODE_REFACTORING_ENABLED", - "version": "undefined" + "version": "5.21.0" }, { "name": "NAVIGATION_REFACTORING", @@ -34,5 +34,13 @@ { "name": "VISA_ONBOARDING_ENABLED", "version": "undefined" + }, + { + "name": "ONRAMP_HOT_TOKENS_ENABLED", + "version": "undefined" + }, + { + "name": "STAKING_TON_ENABLED", + "version": "undefined" } ] diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 3570cea973..128cbb0ab2 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -29,6 +31,7 @@ dependencies { implementation(projects.domain.staking.models) implementation(projects.domain.onramp.models) implementation(projects.domain.models) + implementation(projects.features.onramp.api) /** Tangem libraries */ implementation(deps.tangem.blockchain) @@ -53,6 +56,8 @@ dependencies { implementation(deps.okHttp.prettyLogging) implementation(deps.retrofit) implementation(deps.retrofit.moshi) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) /** Time */ implementation(deps.jodatime) @@ -62,6 +67,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) + debugPGImplementation(deps.chucker) mockedImplementation(deps.chuckerStub) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index 100cc04382..28d4220428 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -40,6 +40,7 @@ sealed class ApiConfig { companion object { internal const val DEBUG_BUILD_TYPE = "debug" + internal const val DEBUG_PG_BUILD_TYPE = "debugPG" internal const val INTERNAL_BUILD_TYPE = "internal" internal const val MOCKED_BUILD_TYPE = "mocked" internal const val EXTERNAL_BUILD_TYPE = "external" diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt index 00945bf772..15c20cc12b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt @@ -1,10 +1,21 @@ package com.tangem.datasource.api.common.config +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + /** * Api environment * [REDACTED_AUTHOR] */ +@JsonClass(generateAdapter = false) enum class ApiEnvironment { - DEV, STAGE, PROD + @Json(name = "DEV") + DEV, + + @Json(name = "STAGE") + STAGE, + + @Json(name = "PROD") + PROD, } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index a0c28a6880..8a850c20ea 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -68,7 +68,9 @@ internal class Express( fun getInitialEnvironment(): ApiEnvironment { return when (BuildConfig.BUILD_TYPE) { - DEBUG_BUILD_TYPE -> ApiEnvironment.DEV + DEBUG_BUILD_TYPE, + DEBUG_PG_BUILD_TYPE, + -> ApiEnvironment.DEV INTERNAL_BUILD_TYPE, MOCKED_BUILD_TYPE, -> ApiEnvironment.STAGE diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt index 2f7ea8a41f..cbc7c36835 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt @@ -1,11 +1,8 @@ package com.tangem.datasource.api.common.config -import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.utils.ProviderSuspend -internal class TangemVisa( - private val authProvider: TangemVisaAuthProvider, -) : ApiConfig() { +internal class TangemVisa : ApiConfig() { override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD @@ -19,7 +16,5 @@ internal class TangemVisa( headers = createHeaders(), ) - private fun createHeaders() = mapOf( - "Authorization" to ProviderSuspend { authProvider.getAuthHeader() }, - ) + private fun createHeaders() = mapOf>() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/visa/TangemVisaAuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/visa/TangemVisaAuthProvider.kt index 62197282df..1f164d8411 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/visa/TangemVisaAuthProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/visa/TangemVisaAuthProvider.kt @@ -2,5 +2,5 @@ package com.tangem.datasource.api.common.visa interface TangemVisaAuthProvider { - suspend fun getAuthHeader(): String + suspend fun getAuthHeader(cardId: String): String } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt index 2d496a94ec..9b63f6b517 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.express.models.request import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class AssetsRequestBody( @Json(name = "tokensList") val tokensList: List?, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/ExchangeSentRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/ExchangeSentRequestBody.kt index b585a80775..7e2c9eae29 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/ExchangeSentRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/ExchangeSentRequestBody.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.express.models.request import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class ExchangeSentRequestBody( @Json(name = "txId") val txId: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/LeastTokenInfo.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/LeastTokenInfo.kt index fd636c6682..9b6884f59e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/LeastTokenInfo.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/LeastTokenInfo.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.express.models.request import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class LeastTokenInfo( @Json(name = "contractAddress") val contractAddress: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt index c22747d9cf..325ac78f23 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.express.models.request import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class PairsRequestBody( @Json(name = "from") val from: List, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt index a4de3fcb40..69e5ccb9a0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class Asset( @Json(name = "contractAddress") val contractAddress: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt index 5e1eb6d5d3..90785de95d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt @@ -1,11 +1,17 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class ExchangeDataResponseWithTxDetails( + @Json(name = "dataResponse") val dataResponse: ExchangeDataResponse, + @Json(name = "txDetails") val txDetails: TxDetails, ) + +@JsonClass(generateAdapter = true) data class ExchangeDataResponse( @Json(name = "fromAmount") val fromAmount: String, @@ -29,6 +35,7 @@ data class ExchangeDataResponse( val signature: String, ) +@JsonClass(generateAdapter = true) data class TxDetails( @Json(name = "payoutAddress") val payoutAddress: String, @@ -70,6 +77,7 @@ data class TxDetails( val gas: String?, ) +@JsonClass(generateAdapter = false) enum class TxType { @Json(name = "send") SEND, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index 2c962257ca..2ef036c212 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import java.math.BigDecimal +@JsonClass(generateAdapter = true) data class ExchangeProvider( @Json(name = "id") val id: String, @@ -32,6 +34,7 @@ data class ExchangeProvider( val slippage: BigDecimal?, ) +@JsonClass(generateAdapter = false) enum class ExchangeProviderType { @Json(name = "dex") DEX, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt index e643811822..d08ebf45bc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import java.math.BigDecimal +@JsonClass(generateAdapter = true) data class ExchangeQuoteResponse( @Json(name = "fromAmount") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeSentResponseBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeSentResponseBody.kt index b6b1453465..284231a50a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeSentResponseBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeSentResponseBody.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class ExchangeSentResponseBody( @Json(name = "txId") val txId: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt index a56308d255..043222d3e3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class ExchangeStatusResponse( @Json(name = "providerId") @@ -26,6 +28,7 @@ data class ExchangeStatusResponse( val refundContractAddress: String? = null, ) +@JsonClass(generateAdapter = false) enum class ExchangeStatus { @Json(name = "new") @@ -71,6 +74,7 @@ enum class ExchangeStatus { Unknown, } +@JsonClass(generateAdapter = true) data class ExchangeStatusError( @Json(name = "code") val code: Int, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt index 183965a431..9d32f36491 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt @@ -1,13 +1,16 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import java.math.BigDecimal +@JsonClass(generateAdapter = true) data class ExpressErrorResponse( @Json(name = "error") val error: ExpressError, ) +@JsonClass(generateAdapter = true) data class ExpressError( @Json(name = "code") val code: Int, @@ -19,6 +22,7 @@ data class ExpressError( val value: ExpressErrorValue?, ) +@JsonClass(generateAdapter = true) data class ExpressErrorValue( @Json(name = "minAmount") val minAmount: String?, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt index f96aef7dfc..9b96730278 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import com.tangem.datasource.api.express.models.request.LeastTokenInfo +@JsonClass(generateAdapter = true) data class SwapPair( @Json(name = "from") val from: LeastTokenInfo, @@ -15,6 +17,7 @@ data class SwapPair( ) +@JsonClass(generateAdapter = true) data class SwapPairProvider( @Json(name = "providerId") val providerId: String, @@ -23,6 +26,7 @@ data class SwapPairProvider( val rateTypes: List, ) +@JsonClass(generateAdapter = false) enum class RateType { @Json(name = "float") FLOAT, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt index e558fdd934..03f14b108c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt @@ -3,8 +3,11 @@ package com.tangem.datasource.api.onramp.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class OnrampDataResponseWithTxDetails( + @Json(name = "dataResponse") val dataResponse: OnrampDataResponse, + @Json(name = "txDetails") val txDetails: OnrampTxDetails, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampStatusResponse.kt index 301fd2421f..c701137912 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampStatusResponse.kt @@ -60,6 +60,7 @@ data class OnrampStatusResponse( val countryCode: String, ) +@JsonClass(generateAdapter = false) enum class Status { @Json(name = "created") Created, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt index 9063ff0b74..efd6c85900 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt @@ -1,11 +1,13 @@ package com.tangem.datasource.api.stakekit.models.request import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody.GasArgs import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO import com.tangem.domain.staking.model.stakekit.action.StakingActionType +@JsonClass(generateAdapter = true) data class PendingActionRequestBody( @Json(name = "type") val type: StakingActionType, @@ -19,6 +21,7 @@ data class PendingActionRequestBody( val gasArgs: GasArgs? = null, // used only in estimate_gas request ) +@JsonClass(generateAdapter = true) data class ActionRequestBody( @Json(name = "integrationId") val integrationId: String, @@ -32,6 +35,7 @@ data class ActionRequestBody( val gasArgs: GasArgs? = null, // used only in estimate_gas request ) +@JsonClass(generateAdapter = true) data class ActionRequestBodyArgs( @Json(name = "amount") val amount: String, @@ -55,6 +59,7 @@ data class ActionRequestBodyArgs( val inputToken: TokenDTO? = null, ) +@JsonClass(generateAdapter = true) data class SignatureVerification( @Json(name = "message") val message: String, @@ -62,6 +67,7 @@ data class SignatureVerification( val signed: String, ) +@JsonClass(generateAdapter = false) enum class TronResource { @Json(name = "ENERGY") ENERGY, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt index 4e93f0353a..fc029f0fb1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.stakekit.models.request import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class Address( @Json(name = "address") val address: String, @@ -11,6 +13,7 @@ data class Address( val explorerUrl: String? = null, ) { + @JsonClass(generateAdapter = true) data class AdditionalAddresses( // cosmos-specific @Json(name = "cosmosPubKey") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ConstructTransactionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ConstructTransactionRequestBody.kt index 60ee34b310..55290861d3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ConstructTransactionRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ConstructTransactionRequestBody.kt @@ -1,14 +1,17 @@ package com.tangem.datasource.api.stakekit.models.request import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import java.math.BigDecimal +@JsonClass(generateAdapter = true) data class ConstructTransactionRequestBody( @Json(name = "gasArgs") val gasArgs: GasArgs? = null, @Json(name = "ledgerWalletAPICompatible") val ledgerWalletAPICompatible: Boolean? = null, ) { + @JsonClass(generateAdapter = true) data class GasArgs( // cosmos-specific @Json(name = "gasPrice") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/MultipleYieldBalancesRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/MultipleYieldBalancesRequestBody.kt index 1872f19673..7b4360ac11 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/MultipleYieldBalancesRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/MultipleYieldBalancesRequestBody.kt @@ -1,19 +1,23 @@ package com.tangem.datasource.api.stakekit.models.request import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class MultipleYieldBalancesRequestBody( @Json(name = "addresses") val addresses: Address, @Json(name = "args") val args: MultipleYieldBalancesRequestArgs, @Json(name = "integrationId") val integrationId: String, ) { + @JsonClass(generateAdapter = true) data class Address( @Json(name = "address") val address: String, @Json(name = "additionalAddresses") val additionalAddresses: AdditionalAddresses? = null, @Json(name = "explorerUrl") val explorerUrl: String, ) { + @JsonClass(generateAdapter = true) data class AdditionalAddresses( @Json(name = "cosmosPubKey") val cosmosPubKey: String? = null, @Json(name = "binanceBeaconAddress") val binanceBeaconAddress: String? = null, @@ -25,6 +29,7 @@ data class MultipleYieldBalancesRequestBody( ) } + @JsonClass(generateAdapter = true) data class MultipleYieldBalancesRequestArgs( @Json(name = "validatorAddresses") val validatorAddresses: List, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/RevenueOption.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/RevenueOption.kt index e396cd7ebb..0d68f38d13 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/RevenueOption.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/RevenueOption.kt @@ -1,8 +1,13 @@ package com.tangem.datasource.api.stakekit.models.request +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) enum class RevenueOption(val value: String) { - SUPPORTS_FEE("supportsFee"), - SUPPORTS_REV_SHARE("supportsRevShare"), + @Json(name = "SUPPORTS_FEE") SUPPORTS_FEE("supportsFee"), + + @Json(name = "SUPPORTS_REV_SHARE") SUPPORTS_REV_SHARE("supportsRevShare"), ; override fun toString(): String { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/SubmitTransactionHashRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/SubmitTransactionHashRequestBody.kt index c07a103fe1..628ff3b1dc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/SubmitTransactionHashRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/SubmitTransactionHashRequestBody.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.stakekit.models.request import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class SubmitTransactionHashRequestBody( @Json(name = "hash") val hash: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt index b950b05390..159d5b9d71 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt @@ -1,13 +1,16 @@ package com.tangem.datasource.api.stakekit.models.request import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class YieldBalanceRequestBody( @Json(name = "addresses") val addresses: Address, @Json(name = "args") val args: YieldBalanceRequestArgs, @Json(name = "integrationId") val integrationId: String, ) { + @JsonClass(generateAdapter = true) data class YieldBalanceRequestArgs( @Json(name = "validatorAddresses") val validatorAddresses: List, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt index c7fedd5ec8..77bfadc65c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.stakekit.models.response.model import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = false) enum class NetworkTypeDTO { @Json(name = "avalanche-c") AVALANCHE_C, @@ -201,5 +203,8 @@ enum class NetworkTypeDTO { @Json(name = "tron") TRON, + @Json(name = "ton") + TON, + UNKNOWN, } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt index 964bde08a7..a83ebdd931 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt @@ -41,6 +41,7 @@ data class BalanceDTO( val providerId: String?, ) { + @JsonClass(generateAdapter = false) enum class BalanceTypeDTO { @Json(name = "available") AVAILABLE, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt index 88cc922696..b58c058063 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt @@ -85,7 +85,7 @@ data class YieldDTO( @Json(name = "preferred") val preferred: Boolean?, ) { - @JsonClass(generateAdapter = true) + @JsonClass(generateAdapter = false) enum class ValidatorStatusDTO { @Json(name = "active") ACTIVE, @@ -99,6 +99,9 @@ data class YieldDTO( @Json(name = "jailed") JAILED, + @Json(name = "full") + FULL, + UNKNOWN, } } @@ -153,6 +156,7 @@ data class YieldDTO( val enabled: Boolean?, ) + @JsonClass(generateAdapter = false) enum class RewardScheduleDTO { @Json(name = "block") BLOCK, @@ -178,6 +182,7 @@ data class YieldDTO( UNKNOWN, } + @JsonClass(generateAdapter = false) enum class RewardClaimingDTO { @Json(name = "auto") AUTO, @@ -189,6 +194,7 @@ data class YieldDTO( } } + @JsonClass(generateAdapter = false) enum class RewardTypeDTO { @Json(name = "apy") APY, // compound rate diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionStatusDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionStatusDTO.kt index 040cab01ea..3695474070 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionStatusDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionStatusDTO.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.stakekit.models.response.model.action import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = false) enum class StakingActionStatusDTO { @Json(name = "CANCELED") CANCELED, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionTypeDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionTypeDTO.kt index 3ba035dc64..7764d9b75b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionTypeDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionTypeDTO.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.stakekit.models.response.model.action import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = false) enum class StakingActionTypeDTO { @Json(name = "STAKE") STAKE, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt index 086837d4b1..34e09b47d5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.stakekit.models.response.model.error import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) class StakeKitErrorResponse( @Json(name = "message") val message: String? = null, @@ -26,6 +28,7 @@ class StakeKitErrorResponse( val tags: List? = null, ) +@JsonClass(generateAdapter = true) data class StakeKitErrorDetailsDTO( @Json(name = "arguments") val arguments: String? = null, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionStatusDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionStatusDTO.kt index f7a4f6d76d..83b7a61515 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionStatusDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionStatusDTO.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.stakekit.models.response.model.transaction import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = false) enum class StakingTransactionStatusDTO { @Json(name = "NOT_FOUND") NOT_FOUND, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionTypeDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionTypeDTO.kt index e79e28e7bb..d129f7c2b9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionTypeDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionTypeDTO.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.stakekit.models.response.model.transaction import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = false) enum class StakingTransactionTypeDTO { @Json(name = "SWAP") SWAP, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 660e465d75..96d2a236d8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -122,6 +122,9 @@ interface TangemTechApi { @Body body: SeedPhraseNotificationDTO, ): ApiResponse + @GET("hot_crypto") + suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse + companion object { val marketsQuoteFields = listOf( "price", diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CoinsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CoinsResponse.kt index 0a19554bf0..de48c2a17a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CoinsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CoinsResponse.kt @@ -1,14 +1,17 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import java.math.BigDecimal +@JsonClass(generateAdapter = true) data class CoinsResponse( @Json(name = "imageHost") val imageHost: String?, @Json(name = "coins") val coins: List, @Json(name = "total") val total: Int, ) { + @JsonClass(generateAdapter = true) data class Coin( @Json(name = "id") val id: String, @Json(name = "name") val name: String, @@ -17,6 +20,7 @@ data class CoinsResponse( @Json(name = "networks") val networks: List = listOf(), ) { + @JsonClass(generateAdapter = true) data class Network( @Json(name = "networkId") val networkId: String, @Json(name = "contractAddress") val contractAddress: String? = null, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserTokensAccountBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserTokensAccountBody.kt index 7f88539dad..d5169e4793 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserTokensAccountBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserTokensAccountBody.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class CreateUserTokensAccountBody( @Json(name = "accountId") val id: Int, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CurrenciesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CurrenciesResponse.kt index c2799b1c2d..786d84a760 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CurrenciesResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CurrenciesResponse.kt @@ -17,7 +17,7 @@ data class CurrenciesResponse( @Json(name = "rateBTC") val rateBTC: String, @Json(name = "unit") val unit: String, // $, €, ₽ @Json(name = "type") val type: String, - val iconSmallUrl: String? = null, - val iconMediumUrl: String? = null, + @Json(name = "iconSmallUrl") val iconSmallUrl: String? = null, + @Json(name = "iconMediumUrl") val iconMediumUrl: String? = null, ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/FeaturesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/FeaturesResponse.kt index 2d72021cd0..eed4e642cd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/FeaturesResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/FeaturesResponse.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class FeaturesResponse( @Json(name = "send") val isNewSendEnabled: Boolean, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/HotCryptoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/HotCryptoResponse.kt new file mode 100644 index 0000000000..14e00f6187 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/HotCryptoResponse.kt @@ -0,0 +1,24 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.math.BigDecimal + +@JsonClass(generateAdapter = true) +data class HotCryptoResponse( + @Json(name = "imageHost") val imageHost: String? = null, + @Json(name = "tokens") val tokens: List, +) { + + @JsonClass(generateAdapter = true) + data class Token( + @Json(name = "id") val id: String, + @Json(name = "symbol") val symbol: String, + @Json(name = "name") val name: String, + @Json(name = "network_id") val networkId: String, + @Json(name = "current_price") val currentPrice: BigDecimal, + @Json(name = "price_change_percentage_24h") val priceChangePercentage: BigDecimal, + @Json(name = "contractAddress") val contractAddress: String? = null, // specific for token + @Json(name = "decimal_count") val decimalCount: Int? = null, // specific for token + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt index e1e9e07e40..8c42687aa8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt @@ -1,13 +1,16 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import java.math.BigDecimal +@JsonClass(generateAdapter = true) data class QuotesResponse( @Json(name = "quotes") val quotes: Map, ) { + @JsonClass(generateAdapter = true) data class Quote( @Json(name = "price") val price: BigDecimal?, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/RatesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/RatesResponse.kt index c743ff9147..4f51d97ee4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/RatesResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/RatesResponse.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass // rates.keys = networkId's +@JsonClass(generateAdapter = true) data class RatesResponse( @Json(name = "rates") val rates: Map, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt index 2abb6e2a57..0327383d27 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt @@ -1,30 +1,36 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import org.joda.time.LocalDate +@JsonClass(generateAdapter = true) data class ReferralResponse( @Json(name = "conditions") val conditions: Conditions, @Json(name = "referral") val referral: Referral?, @Json(name = "expectedAwards") val expectedAwards: ExpectedAwards?, ) { + @JsonClass(generateAdapter = true) data class Conditions( @Json(name = "discount") val discount: Discount, @Json(name = "awards") val awards: List, @Json(name = "tosLink") val tosLink: String, ) { + @JsonClass(generateAdapter = true) data class Discount( @Json(name = "amount") val amount: Int, @Json(name = "type") val discountType: String, ) + @JsonClass(generateAdapter = true) data class Award( @Json(name = "amount") val amount: Int, @Json(name = "token") val token: Token, ) { + @JsonClass(generateAdapter = true) data class Token( @Json(name = "id") val id: String, @Json(name = "name") val name: String, @@ -36,6 +42,7 @@ data class ReferralResponse( } } + @JsonClass(generateAdapter = true) data class Referral( @Json(name = "shareLink") val shareLink: String, @Json(name = "address") val address: String, @@ -44,11 +51,13 @@ data class ReferralResponse( @Json(name = "termsAcceptedAt") val termsAcceptedAt: String?, ) + @JsonClass(generateAdapter = true) data class ExpectedAwards( @Json(name = "numberOfWallets") val numberOfWallets: Int, @Json(name = "list") val list: List, ) { + @JsonClass(generateAdapter = true) data class AwardItem( @Json(name = "currency") val currency: String, @Json(name = "paymentDate") val paymentDate: LocalDate, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SalesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SalesResponse.kt index 1c3461f012..c9c923f823 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SalesResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SalesResponse.kt @@ -1,10 +1,12 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass /** * Sales response */ +@JsonClass(generateAdapter = true) data class SalesResponse( @Json(name = "sales") val sales: List, ) @@ -17,6 +19,7 @@ data class SalesResponse( * @property product product that is sales * @property notification optional notification for product */ +@JsonClass(generateAdapter = true) data class Sales( @Json(name = "id") val id: String, @Json(name = "state") val state: String, @@ -31,12 +34,14 @@ data class Sales( * @property code code that shows what product it is * @property name product name */ +@JsonClass(generateAdapter = true) data class Product( @Json(name = "id") val id: String, @Json(name = "code") val code: String, @Json(name = "name") val name: String, ) +@JsonClass(generateAdapter = true) data class Notification( @Json(name = "type") val type: String, @Json(name = "title") val title: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ShopResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ShopResponse.kt index 6331011641..1c1c273265 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ShopResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ShopResponse.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass /** * Shop response @@ -9,6 +10,7 @@ import com.squareup.moshi.Json * [REDACTED_AUTHOR] */ +@JsonClass(generateAdapter = true) data class ShopResponse( @Json(name = "canOrder") val isOrderingAvailable: Boolean, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/StartReferralBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/StartReferralBody.kt index 8f2c933f19..d18541e7f2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/StartReferralBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/StartReferralBody.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass /** Body for make user referral */ +@JsonClass(generateAdapter = true) data class StartReferralBody( @Json(name = "walletId") val walletId: String, @Json(name = "networkId") val networkId: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UpdateUserTokensAccountBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UpdateUserTokensAccountBody.kt index 2413b85d2b..0ba0f997d5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UpdateUserTokensAccountBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UpdateUserTokensAccountBody.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class UpdateUserTokensAccountBody( @Json(name = "accountTitle") val title: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensAccountResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensAccountResponse.kt index 0e8341612d..f4e75add0a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensAccountResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensAccountResponse.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class UserTokensAccountResponse( @Json(name = "accountId") val id: Int, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index dcd3107e06..46432b2d8c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import com.tangem.common.extensions.calculateHashCode +@JsonClass(generateAdapter = true) data class UserTokensResponse( @Json(name = "version") val version: Int = 0, @Json(name = "group") val group: GroupType, @@ -10,6 +12,7 @@ data class UserTokensResponse( @Json(name = "tokens") val tokens: List = emptyList(), ) { + @JsonClass(generateAdapter = true) data class Token( @Json(name = "id") val id: String? = null, @Json(name = "networkId") val networkId: String, @@ -36,6 +39,7 @@ data class UserTokensResponse( ) } + @JsonClass(generateAdapter = false) enum class GroupType { @Json(name = "none") NONE, @@ -47,6 +51,7 @@ data class UserTokensResponse( NETWORK, } + @JsonClass(generateAdapter = false) enum class SortType { @Json(name = "balance") BALANCE, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensSettingsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensSettingsResponse.kt index 9d5a0554cf..84b17f2677 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensSettingsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensSettingsResponse.kt @@ -1,7 +1,9 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class UserTokensSettingsResponse( @Json(name = "group") val group: UserTokensResponse.GroupType, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt index fdb8a0df7c..e03b095d4e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt @@ -1,13 +1,16 @@ package com.tangem.datasource.api.tangemTech.models.v2 import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +@JsonClass(generateAdapter = true) data class UserTokensResponseV2( @Json(name = "accounts") val accounts: List, ) { + @JsonClass(generateAdapter = true) data class TokensAccount( @Json(name = "id") val id: Int, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt index afdded620a..57cb30dab0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt @@ -1,10 +1,21 @@ package com.tangem.datasource.api.visa +import com.tangem.datasource.api.utils.ReadTimeout import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse import retrofit2.http.GET +import retrofit2.http.Header +import java.util.concurrent.TimeUnit interface TangemVisaApi { @GET("activation-status") - suspend fun getRemoteActivationStatus(): CardActivationRemoteStateResponse + suspend fun getRemoteActivationStatus( + @Header("Authorization") authHeader: String, + ): CardActivationRemoteStateResponse + + @ReadTimeout(duration = 20, TimeUnit.MINUTES) + @GET("activation-status") + suspend fun getRemoteActivationStatusLongPoll( + @Header("Authorization") authHeader: String, + ): CardActivationRemoteStateResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index a9576c3042..4f6517e5ff 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -5,7 +5,6 @@ import com.tangem.datasource.api.common.config.* import com.tangem.datasource.api.common.config.Express import com.tangem.datasource.api.common.config.StakeKit import com.tangem.datasource.api.common.config.TangemTech -import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider @@ -47,6 +46,5 @@ internal object ApiConfigsModule { @Provides @IntoSet - fun provideTangemVisaConfig(tangemVisaAuthProvider: TangemVisaAuthProvider): ApiConfig = - TangemVisa(tangemVisaAuthProvider) + fun provideTangemVisaConfig(): ApiConfig = TangemVisa() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 069f85942e..29dbfa7630 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -194,7 +194,7 @@ internal object NetworkModule { clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), - ) + ).applyTimeoutAnnotations() }, ) } @@ -215,7 +215,7 @@ internal object NetworkModule { clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), - ) + ).applyTimeoutAnnotations() }, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/HotCryptoLoaderModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/HotCryptoLoaderModule.kt new file mode 100644 index 0000000000..2e651928df --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/HotCryptoLoaderModule.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.di.exchangeservice + +import com.tangem.datasource.exchangeservice.hotcrypto.DefaultHotCryptoLoader +import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface HotCryptoLoaderModule { + + @Binds + @Singleton + fun bindExpressServiceLoader(defaultHotCryptoLoader: DefaultHotCryptoLoader): HotCryptoLoader +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/DefaultHotCryptoLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/DefaultHotCryptoLoader.kt new file mode 100644 index 0000000000..6ca3229ae4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/DefaultHotCryptoLoader.kt @@ -0,0 +1,81 @@ +package com.tangem.datasource.exchangeservice.hotcrypto + +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.onramp.OnrampFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import timber.log.Timber +import javax.inject.Inject + +/** + * Default implementation of [HotCryptoLoader] + * + * @property tangemTechApi tangem tech api + * @property hotCryptoResponseStore store of [HotCryptoResponse] + * @property appPreferencesStore app preferences store + * @property dispatchers dispatchers + * @property onrampFeatureToggles onramp feature toggles + * +[REDACTED_AUTHOR] + */ +internal class DefaultHotCryptoLoader @Inject constructor( + private val tangemTechApi: TangemTechApi, + private val hotCryptoResponseStore: HotCryptoResponseStore, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, + private val onrampFeatureToggles: OnrampFeatureToggles, +) : HotCryptoLoader { + + override suspend fun fetch(tokens: List) { + updateInternal { hotToken -> + tokens.none { + it.id == hotToken.id && it.contractAddress == hotToken.contractAddress && + it.networkId == hotToken.networkId + } + } + } + + override suspend fun update(currencies: List) { + updateInternal { hotToken -> + currencies.none { + it.id.rawCurrencyId?.value == hotToken.id && + (it as? CryptoCurrency.Token)?.contractAddress == hotToken.contractAddress && + it.network.id.value == hotToken.networkId + } + } + } + + private suspend fun updateInternal(filterPredicate: (HotCryptoResponse.Token) -> Boolean) { + if (!onrampFeatureToggles.isHotTokensEnabled) return + + runCatching(dispatchers.io) { + tangemTechApi.getHotCrypto(currencyId = getAppCurrencyId()).getOrThrow() + } + .onSuccess { + Timber.d("HotCrypto is successfully updated") + + hotCryptoResponseStore.store( + value = it.copy(tokens = it.tokens.filter(filterPredicate)), + ) + } + .onFailure { + Timber.e(it, "Unable to fetch hot crypto") + } + } + + private suspend fun getAppCurrencyId(): String { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, + ) + ?.id + ?: "usd" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoLoader.kt new file mode 100644 index 0000000000..05536e4e8d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoLoader.kt @@ -0,0 +1,26 @@ +package com.tangem.datasource.exchangeservice.hotcrypto + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.tokens.model.CryptoCurrency + +/** + * Hot crypto loader + * +[REDACTED_AUTHOR] + */ +interface HotCryptoLoader { + + /** + * Fetch hot crypto + * + * @param tokens already added tokens + */ + suspend fun fetch(tokens: List) + + /** + * Update hot crypto + * + * @param currencies already added crypto currencies + */ + suspend fun update(currencies: List) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoResponseStore.kt new file mode 100644 index 0000000000..fcfb0a901a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/hotcrypto/HotCryptoResponseStore.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.exchangeservice.hotcrypto + +import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse +import com.tangem.datasource.local.datastore.RuntimeStateStore +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Store of [HotCryptoResponse] + * +[REDACTED_AUTHOR] + */ +@Singleton +class HotCryptoResponseStore @Inject constructor() : RuntimeStateStore by RuntimeStateStore( + defaultValue = HotCryptoResponse(imageHost = null, tokens = emptyList()), +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/card/UsedCardInfo.kt b/core/datasource/src/main/java/com/tangem/datasource/local/card/UsedCardInfo.kt index 0f619b1eec..2e0064720d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/card/UsedCardInfo.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/card/UsedCardInfo.kt @@ -1,8 +1,16 @@ package com.tangem.datasource.local.card +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) data class UsedCardInfo( + @Json(name = "cardId") val cardId: String, + @Json(name = "isScanned") val isScanned: Boolean = false, + @Json(name = "isActivationStarted") val isActivationStarted: Boolean = false, + @Json(name = "isActivationFinished") val isActivationFinished: Boolean = false, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/providers/models/ProviderModel.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/providers/models/ProviderModel.kt index 328618476a..88071995fe 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/providers/models/ProviderModel.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/providers/models/ProviderModel.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.local.config.providers.models import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass /** Config provider model */ sealed class ProviderModel { @@ -12,6 +13,7 @@ sealed class ProviderModel { * "url": "https://example.com" * } */ + @JsonClass(generateAdapter = true) data class Public( @Json(name = "url") val url: String, ) : ProviderModel() @@ -23,6 +25,7 @@ sealed class ProviderModel { * "name": "nownodes" * } */ + @JsonClass(generateAdapter = true) data class Private( @Json(name = "name") val name: String, ) : ProviderModel() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt index bb1112d268..ac6d80ef03 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.local.logs import android.content.Context +import com.tangem.datasource.BuildConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineExceptionHandler @@ -58,26 +59,30 @@ class AppLogsStore @Inject constructor( fun getFile(): File? = if (file.exists()) file else null /** Save log [message] */ - fun saveLogMessage(message: String) { - // Temporally logs are not saved - return + fun saveLogMessage(tag: String, message: String) { + // Temporally logs are not saved in prod environment + if (!BuildConfig.TESTER_MENU_ENABLED) { + return + } launchWithLock { createFileIfNotExist() - writeMessage(message) + writeMessage(tag = tag, message) } } /** Save log that consists from [messages] */ - fun saveLogMessage(vararg messages: String) { - // Temporally logs are not saved - return + fun saveLogMessage(tag: String, vararg messages: String) { + // Temporally logs are not saved in prod environment + if (!BuildConfig.TESTER_MENU_ENABLED) { + return + } launchWithLock { createFileIfNotExist() - writeMessage(*messages) + writeMessage(tag = tag, *messages) } } @@ -102,10 +107,10 @@ class AppLogsStore @Inject constructor( if (file.exists()) file.delete() } - private fun writeMessage(vararg messages: String) { + private fun writeMessage(tag: String, vararg messages: String) { BufferedWriter(FileWriter(file, true)).use { writer -> writer.append(formatter.print(DateTime.now())) - writer.append(": ") + writer.append(": $tag ") messages.forEach(writer::append) writer.newLine() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt index 9e326cc62c..daa20726ae 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt @@ -11,6 +11,9 @@ import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.preferencesDataStoreFile import com.tangem.datasource.local.preferences.PreferencesDataStore.INSTANCE import com.tangem.datasource.local.preferences.PreferencesKeys.APP_LOGS_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY import com.tangem.datasource.local.preferences.utils.CleanupKeyMigration import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration import kotlinx.coroutines.CoroutineScope @@ -80,6 +83,9 @@ internal object PreferencesDataStore { keyName = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY.name, ), CleanupKeyMigration(key = APP_LOGS_KEY), + CleanupKeyMigration(key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY), + CleanupKeyMigration(key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY), + CleanupKeyMigration(key = SHOULD_SHOW_RING_PROMO_KEY), ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 0a5aad5199..bcabd0214c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -17,6 +17,9 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_TWINS_ONBOARD /** * All preferences keys that DataStore is stored. * + * !!!IMPORTANT!!! + * Before DELETING active key do [CleanupKeyMigration] in [PreferencesDataStore] + * [REDACTED_AUTHOR] */ object PreferencesKeys { @@ -87,10 +90,12 @@ object PreferencesKeys { val UNSUBMITTED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "unsubmittedTransactions") } + @Deprecated("Remove after CleanupKeyMigration") val IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY by lazy { booleanPreferencesKey(name = "isWalletSwapPromoOkxShown") } + @Deprecated("Remove after CleanupKeyMigration") val IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY by lazy { booleanPreferencesKey(name = "isTokenSwapPromoOkxShown") } @@ -99,6 +104,7 @@ object PreferencesKeys { val ADDED_WALLETS_WITH_RING_KEY by lazy { stringSetPreferencesKey(name = "addedWalletsWithRing") } + @Deprecated("Remove after CleanupKeyMigration") val SHOULD_SHOW_RING_PROMO_KEY by lazy { booleanPreferencesKey(name = "shouldShowRingPromo") } val ONRAMP_DEFAULT_COUNTRY by lazy { stringPreferencesKey(name = "onrampDefaultCountry") } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index fc0d1e0e42..962b4afddd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -11,11 +11,9 @@ internal class DefaultQuotesStore( private val dataStore: StringKeyDataStore, ) : QuotesStore { - override fun get(currenciesIds: Set): Flow> { + override fun get(currenciesIds: Set): Flow> { return channelFlow { - val flows = currenciesIds.mapNotNull { currencyId -> - currencyId.rawCurrencyId?.let(dataStore::get) - } + val flows = currenciesIds.map { currencyId -> dataStore.get(currencyId.value) } if (dataStore.isEmpty() || flows.isEmpty()) { send(emptySet()) @@ -30,12 +28,8 @@ internal class DefaultQuotesStore( } } - override suspend fun getSync(currenciesIds: Set): Set { - return currenciesIds.mapNotNull { currencyId -> - currencyId.rawCurrencyId?.let { - dataStore.getSyncOrNull(it) - } - }.toSet() + override suspend fun getSync(currenciesIds: Set): Set { + return currenciesIds.mapNotNull { currencyId -> dataStore.getSyncOrNull(currencyId.value) }.toSet() } override suspend fun store(response: QuotesResponse) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt index dd3f577782..38f90100e4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt @@ -7,9 +7,9 @@ import kotlinx.coroutines.flow.Flow interface QuotesStore { - fun get(currenciesIds: Set): Flow> + fun get(currenciesIds: Set): Flow> - suspend fun getSync(currenciesIds: Set): Set + suspend fun getSync(currenciesIds: Set): Set suspend fun store(response: QuotesResponse) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaAuthTokenStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaAuthTokenStorage.kt index 92ffe1073b..a91c2705a1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaAuthTokenStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaAuthTokenStorage.kt @@ -4,7 +4,9 @@ import com.tangem.domain.visa.model.VisaAuthTokens interface VisaAuthTokenStorage { - suspend fun store(tokens: VisaAuthTokens) + suspend fun store(cardId: String, tokens: VisaAuthTokens) - suspend fun get(): VisaAuthTokens? + suspend fun get(cardId: String): VisaAuthTokens? + + fun remove(cardId: String) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index a4dd795246..c3b1cfb588 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -44,17 +44,13 @@ internal fun OkHttpClient.Builder.applyTimeoutAnnotations(): OkHttpClient.Builde val readTimeout = tag?.method()?.getAnnotation(ReadTimeout::class.java) val writeTimeout = tag?.method()?.getAnnotation(WriteTimeout::class.java) - chain - .apply { - connectionTimeout?.let { withConnectTimeout(timeout = it.duration, unit = it.unit) } - } - .apply { - readTimeout?.let { withReadTimeout(timeout = it.duration, unit = it.unit) } - } - .apply { - writeTimeout?.let { withWriteTimeout(timeout = it.duration, unit = it.unit) } - } - .proceed(request) + chain.run { + connectionTimeout?.let { withConnectTimeout(timeout = it.duration, unit = it.unit) } ?: this + }.run { + readTimeout?.let { withReadTimeout(timeout = it.duration, unit = it.unit) } ?: this + }.run { + writeTimeout?.let { withWriteTimeout(timeout = it.duration, unit = it.unit) } ?: this + }.proceed(request) }, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt index b593688bd9..26703087a9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt @@ -40,7 +40,7 @@ internal class NetworkLogsSaveInterceptor( try { response = chain.proceed(request) } catch (e: Exception) { - appLogsStore.saveLogMessage("<-- HTTP FAILED: $e") + saveLogMessage("<-- HTTP FAILED: $e") throw e } @@ -53,7 +53,7 @@ internal class NetworkLogsSaveInterceptor( val connection = chain.connection() val connectionProtocol = if (connection != null) " ${connection.protocol()}" else "" - appLogsStore.saveLogMessage( + saveLogMessage( "--> ${request.method} ${request.url}$connectionProtocol\n", createRequestEndMessage(request), ) @@ -136,7 +136,7 @@ internal class NetworkLogsSaveInterceptor( val spaceBeforeResponseMessage = if (response.message.isEmpty()) "" else ' ' + response.message - appLogsStore.saveLogMessage( + saveLogMessage( "<-- ${response.code}", spaceBeforeResponseMessage, response.message, @@ -198,4 +198,8 @@ internal class NetworkLogsSaveInterceptor( null } } + + private fun saveLogMessage(vararg messages: String) { + appLogsStore.saveLogMessage(tag = "NetworkLogs", *messages) + } } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 5d92d7abd8..0b65dbcbf0 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -6,16 +6,15 @@ import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.config.* import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_BUILD_TYPE +import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_PG_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE -import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.utils.ProviderSuspend import com.tangem.utils.version.AppVersionProvider -import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.runBlocking @@ -31,7 +30,6 @@ private val appVersionProvider = mockk() private val expressAuthProvider = mockk() private val stakeKitAuthProvider = mockk() private val appAuthProvider = mockk() -private val visaAuthProvider = mockk() // Don't forget to add new config !!! private val API_CONFIGS = setOf( @@ -39,7 +37,7 @@ private val API_CONFIGS = setOf( TangemTech(appVersionProvider, appAuthProvider), StakeKit(stakeKitAuthProvider), TangemVisaAuth(), - TangemVisa(visaAuthProvider), + TangemVisa(), ) /** @@ -59,7 +57,6 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY every { appAuthProvider.getCardId() } returns APP_CARD_ID every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY - coEvery { visaAuthProvider.getAuthHeader() } returns VISA_AUTH_HEADER } @Test @@ -84,7 +81,6 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { const val STAKE_KIT_API_KEY = "stake_kit_api_key" const val APP_CARD_ID = "app_card_id" const val APP_CARD_PUBLIC_KEY = "app_public_key" - const val VISA_AUTH_HEADER = "Bearer visa_auth_header" @JvmStatic @Parameterized.Parameters @@ -100,7 +96,9 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { private fun createExpressModel(): Model { val environment = when (BuildConfig.BUILD_TYPE) { - DEBUG_BUILD_TYPE -> ApiEnvironment.DEV + DEBUG_BUILD_TYPE, + DEBUG_PG_BUILD_TYPE, + -> ApiEnvironment.DEV INTERNAL_BUILD_TYPE, MOCKED_BUILD_TYPE, -> ApiEnvironment.STAGE @@ -115,7 +113,9 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { expected = ApiEnvironmentConfig( environment = environment, baseUrl = when (BuildConfig.BUILD_TYPE) { - DEBUG_BUILD_TYPE -> "[REDACTED_ENV_URL]" + DEBUG_BUILD_TYPE, + DEBUG_PG_BUILD_TYPE, + -> "[REDACTED_ENV_URL]" INTERNAL_BUILD_TYPE, MOCKED_BUILD_TYPE, -> "[REDACTED_ENV_URL]" @@ -198,9 +198,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { expected = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, baseUrl = "https://bff.tangem.com/", - headers = mapOf( - "Authorization" to ProviderSuspend { VISA_AUTH_HEADER }, - ), + headers = mapOf(), ), ) } diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 0fc0d39a01..aa9e27fb7d 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -788,6 +788,8 @@ The period you must wait after requesting to withdraw funds from staking before the tokens become available. Warmup period The allocated time for activating participation in staking. + No available validators at the moment. Please try again later. + Staking Unavailable The network will charge a token approval fee to verify that you are authorizing the use of your token for the staking. By using staking functionality, you agree with provider’s %1$s and %2$s Locked diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt index 380c6bd97b..27c758f8b7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt @@ -1,121 +1,20 @@ package com.tangem.core.ui.components import android.content.res.Configuration -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.Surface -import androidx.compose.material.Text +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R -import com.tangem.core.ui.components.states.Item -import com.tangem.core.ui.components.states.SelectableItemsState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme -import com.valentinilk.shimmer.shimmer -import kotlinx.collections.immutable.toImmutableList - -/** - * Small card with text information attached to the edges - * - * @param startText text information attached to the left edge - * @param endText text information attached to the right edge - * @param isLoading if true, shimmer is shown instead of [startText] and [endText] - * - * @see Figma component - */ -@Composable -fun SmallInfoCard(startText: String, endText: String, isLoading: Boolean = false) { - Surface( - shape = RoundedCornerShape(TangemTheme.dimens.radius12), - color = TangemTheme.colors.background.primary, - elevation = TangemTheme.dimens.elevation2, - ) { - CardInfoBox( - startText = startText, - endText = endText, - isLoading = isLoading, - disclaimer = null, - ) - } -} - -/** - * Small card with text information attached to the edges - * - * @param startText text information attached to the left edge - * @param endText text information attached to the right edge - * @param disclaimer closable description text - * @param isLoading if true, shimmer is shown instead of [startText] and [endText] - * - * @see Figma component - */ -@Composable -fun SmallInfoCardWithDisclaimer(startText: String, endText: String, disclaimer: String, isLoading: Boolean = false) { - Surface( - shape = RoundedCornerShape(TangemTheme.dimens.radius12), - color = TangemTheme.colors.background.primary, - elevation = TangemTheme.dimens.elevation2, - ) { - CardInfoBox( - startText = startText, - endText = endText, - isLoading = isLoading, - disclaimer = disclaimer, - ) - } -} - -/** - * [SmallInfoCard] with warning information shown underneath it. - * - * @param startText text information attached to the left edge - * @param endText text information attached to the right edge - * @param warningText text of the warning - * @param disclaimer closable description text - * - * @see Figma component - */ -@Composable -fun SmallInfoCardWithWarning(startText: String, endText: String, warningText: String, disclaimer: String? = null) { - Surface( - shape = RoundedCornerShape(TangemTheme.dimens.size12), - color = TangemTheme.colors.background.primary, - elevation = TangemTheme.dimens.elevation2, - ) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - CardInfoBox(startText = startText, endText = endText, disclaimer = disclaimer) - - Divider( - color = TangemTheme.colors.stroke.primary, - thickness = TangemTheme.dimens.size0_5, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), - ) - - WarningItem(warningText = warningText) - } - } -} /** * Small card with an icon to the left and title with description shown to the right of it. @@ -140,7 +39,8 @@ fun CardWithIcon( Surface( shape = RoundedCornerShape(TangemTheme.dimens.radius12), color = TangemTheme.colors.background.primary, - elevation = TangemTheme.dimens.elevation2, + tonalElevation = TangemTheme.dimens.elevation2, + shadowElevation = TangemTheme.dimens.elevation2, ) { IconWithTitleAndDescription( title = title, @@ -223,393 +123,12 @@ internal fun IconWithTitleAndDescription( } } -/** - * Card with text information attached to the edges and selectable items - * - * @param state state of block with items - * @param isLoading if true, shimmer is shown instead of data - * @param disclaimer description text - * @param onSelect listener on item selection - * - * @see Figma component - */ -@Composable -fun SelectableInfoCard( - state: SelectableItemsState, - onSelect: (Item) -> Unit, - isLoading: Boolean = false, - disclaimer: String? = null, -) { - Surface( - shape = RoundedCornerShape(TangemTheme.dimens.radius12), - color = TangemTheme.colors.background.primary, - elevation = TangemTheme.dimens.elevation2, - ) { - SelectableInfoBlock( - state = state, - isLoading = isLoading, - disclaimer = disclaimer, - onSelect = onSelect, - ) - } -} - -/** - * Card with text information attached to the edges, selectable items and warning - * - * @param state state of block with items - * @param isLoading if true, shimmer is shown instead of data - * @param disclaimer description text - * @param onSelect listener on item selection - * - * @see Figma component - */ -@Composable -fun SelectableInfoCardWithWarning( - state: SelectableItemsState, - warningText: String, - onSelect: (Item) -> Unit, - isLoading: Boolean = false, - disclaimer: String? = null, -) { - Surface( - shape = RoundedCornerShape(TangemTheme.dimens.radius12), - color = TangemTheme.colors.background.primary, - elevation = TangemTheme.dimens.elevation2, - ) { - SelectableInfoBlock( - state = state, - isLoading = isLoading, - disclaimer = disclaimer, - warningText = warningText, - onSelect = onSelect, - ) - } -} - -// region elements - -@Composable -private fun CardInfoBox(startText: String, endText: String, disclaimer: String?, isLoading: Boolean = false) { - val isOpened = remember { mutableStateOf(false) } - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(TangemTheme.dimens.size48) - .clickable { isOpened.value = !isOpened.value } - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing12, - ), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = startText, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - style = TangemTheme.typography.subtitle2, - ) - - if (isLoading) { - ShimmerItem(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12) - } else { - Row { - Text( - text = endText, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - style = TangemTheme.typography.body2, - ) - if (disclaimer != null) { - val chevronIcon = if (isOpened.value) { - painterResource(id = R.drawable.ic_chevron_up_24) - } else { - painterResource(id = R.drawable.ic_chevron_24) - } - Icon( - modifier = Modifier.size(TangemTheme.dimens.size20), - painter = chevronIcon, - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } - } - } - } - if (isOpened.value && disclaimer != null) { - DisclaimerItem(disclaimer) - } - } -} - -@Suppress("LongMethod") -@Composable -private fun SelectableInfoBlock( - state: SelectableItemsState, - onSelect: (Item) -> Unit, - isLoading: Boolean = false, - disclaimer: String? = null, - warningText: String? = null, -) { - var isOpened by remember { mutableStateOf(false) } - Column { - if (isLoading) { - ShimmerItem(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12) - } else { - val selectedItem = state.selectedItem - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(TangemTheme.dimens.size48) - .clickable { isOpened = !isOpened } - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing12, - ), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = selectedItem.startText.resolveReference(), - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - style = TangemTheme.typography.subtitle2, - ) - - Row { - Text( - text = selectedItem.endText.resolveReference(), - color = TangemTheme.colors.text.primary1, - maxLines = 1, - style = TangemTheme.typography.body2, - ) - val chevronIcon = if (isOpened) { - painterResource(id = R.drawable.ic_chevron_up_24) - } else { - painterResource(id = R.drawable.ic_chevron_24) - } - Icon( - modifier = Modifier.size(TangemTheme.dimens.size20), - painter = chevronIcon, - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } - } - - Divider( - color = TangemTheme.colors.stroke.primary, - thickness = TangemTheme.dimens.size0_5, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), - ) - - if (isOpened) { - state.items.forEach { item -> - SelectableItem(item) { - onSelect.invoke(item) - } - Divider( - color = TangemTheme.colors.stroke.primary, - thickness = TangemTheme.dimens.size0_5, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), - ) - } - if (disclaimer != null) { - DisclaimerItem(disclaimer) - Divider( - color = TangemTheme.colors.stroke.primary, - thickness = TangemTheme.dimens.size0_5, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), - ) - } - } - if (warningText?.isNotEmpty() == true) { - WarningItem(warningText = warningText) - } - } - } -} - -@Composable -private fun SelectableItem(item: Item, onSelect: () -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(TangemTheme.dimens.size48) - .clickable { onSelect() } - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing12, - ), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = item.startText.resolveReference(), - color = TangemTheme.colors.text.primary1, - maxLines = 1, - style = TangemTheme.typography.subtitle2, - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = item.endText.resolveReference(), - color = TangemTheme.colors.text.primary1, - maxLines = 1, - style = TangemTheme.typography.body2, - ) - val size = TangemTheme.dimens.size24 - if (item.isSelected) { - Icon( - modifier = Modifier.size(size), - painter = painterResource(id = R.drawable.ic_check_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - } else { - SpacerW(width = size) - } - } - } -} - -@Composable -private fun ShimmerItem(width: Dp, height: Dp) { - Box( - modifier = Modifier - .size(width = width, height = height) - .shimmer() - .background( - color = TangemTheme.colors.button.secondary, - shape = RoundedCornerShape(TangemTheme.dimens.radius3), - ), - ) -} - -@Composable -private fun DisclaimerItem(disclaimer: String) { - Divider( - color = TangemTheme.colors.stroke.primary, - thickness = TangemTheme.dimens.size0_5, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - top = TangemTheme.dimens.spacing14, - bottom = TangemTheme.dimens.spacing16, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - ), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .background( - color = TangemTheme.colors.background.secondary, - shape = CircleShape, - ) - .height(TangemTheme.dimens.size40) - .width(TangemTheme.dimens.size40), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_heart_24), - tint = TangemTheme.colors.icon.secondary, - contentDescription = null, - ) - } - SpacerW12() - Text( - text = disclaimer, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - ) - } -} - -@Composable -private fun WarningItem(warningText: String) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - top = TangemTheme.dimens.spacing14, - bottom = TangemTheme.dimens.spacing16, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - ), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .background( - color = TangemTheme.colors.background.secondary, - shape = CircleShape, - ) - .height(TangemTheme.dimens.size40) - .width(TangemTheme.dimens.size40), - contentAlignment = Alignment.Center, - ) { - Image( - painter = painterResource(id = R.drawable.img_attention_20), - contentDescription = null, - ) - } - SpacerW12() - Text( - text = warningText, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body2, - ) - } -} - -// endregion elements - // region Preview - -@Suppress("MagicNumber") +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun CardsPreview() { - Column(modifier = Modifier.fillMaxWidth()) { - SmallInfoCard( - startText = "Balance", - endText = "0.4405434 BTC", - ) - - SpacerH32() - - SmallInfoCardWithDisclaimer( - startText = "Balance", - endText = "0.4405434 BTC", - disclaimer = "Not enough funds for fee on your Polygon wallet to create a transaction." + - "Top up your Polygon wallet first.", - ) - - SpacerH32() - - SmallInfoCardWithWarning( - startText = "Balance", - endText = "0.4405434 BTC", - warningText = "Not enough funds for fee on your Polygon wallet to create a transaction. " + - "Top up your Polygon wallet first.", - disclaimer = "Not enough funds for fee on your Polygon wallet to create a transaction." + - "Top up your Polygon wallet first.", - ) - - SpacerH32() - +private fun Preview_CardWithIcon() { + TangemThemePreview { CardWithIcon( title = "Permit is valid until", description = "26:30", @@ -621,52 +140,6 @@ private fun CardsPreview() { ) }, ) - - SpacerH32() - - val state = SelectableItemsState( - selectedItem = Item(0, TextReference.Str("Balance"), TextReference.Str("0.4405434 BTC"), true, ""), - items = listOf( - Item(0, TextReference.Str("Normal"), TextReference.Str("0.4405434 BTC"), true, ""), - Item(1, TextReference.Str("Priority"), TextReference.Str("0.46 BTC"), false, ""), - ).toImmutableList(), - ) - SelectableInfoCard( - state = state, - isLoading = false, - disclaimer = "Not enough funds for fee on your Polygon wallet to create a transaction", - onSelect = {}, - ) - - SpacerH32() - - SelectableInfoCardWithWarning( - state = state, - isLoading = false, - warningText = "Not enough funds for fee on your Polygon wallet to create a transaction. " + - "Top up your Polygon wallet first.", - disclaimer = "Not enough funds for fee on your Polygon wallet to create a transaction", - onSelect = {}, - ) - } -} - -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_Cards() { - TangemThemePreview { - CardsPreview() - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Preview(widthDp = 328, heightDp = 48, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_SimpleInfoCard() { - TangemThemePreview { - SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC") - SmallInfoCardWithDisclaimer(startText = "Balance", endText = "0.4405434 BTC", disclaimer = "test") } } // endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt deleted file mode 100644 index cee6370513..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt +++ /dev/null @@ -1,209 +0,0 @@ -package com.tangem.core.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme - -/** - * A card with a warning icon to the left and title with description shown to the right of it. - * - * @param title title of the warning in bold - * @param description description of the warning - * - * @see Figma component - */ -@Composable -fun WarningCard(title: String, description: String, icon: @Composable (() -> Unit)? = null) { - WarningCardSurface( - content = { - WarningBody( - title = title, - description = description, - icon = icon, - ) - }, - ) -} - -/** - * A card with a warning icon to the left and title without description shown to the right of it. - * - * @param title title of the warning in bold - * - * @see Figma component - */ -@Composable -fun WarningCardTitleOnly(title: String, icon: @Composable (() -> Unit)? = null) { - WarningCardMaterial3Style( - content = { - WarningBody( - title = title, - description = null, - icon = icon, - iconBackground = TangemTheme.colors.button.disabled, - ) - }, - ) -} - -/** - * [WarningCard], but clickable (with an 'greater then' icon to the left) - * - * @param title title of the warning in bold - * @param description description of the warning - * @param onClick action to be performed when warning is clicked - * - * @see Figma component - */ -@Composable -fun ClickableWarningCard( - title: String, - description: String, - icon: @Composable (() -> Unit)? = null, - onClick: () -> Unit, -) { - WarningCardSurface( - content = { - WarningBody(title = title, description = description, icon = icon) { - SpacerW12() - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - }, - onClick = onClick, - ) -} - -/** - * [WarningCard], but clickable and with a 'refresh' icon at the right edge - * - * @param title title of the warning in bold - * @param description description of the warning - * @param onClick action to be performed when warning is clicked - * - * @see Figma component - */ -@Composable -fun RefreshableWarningCard( - title: String, - description: String, - icon: @Composable (() -> Unit)? = null, - onClick: () -> Unit, -) { - WarningCardSurface( - content = { - WarningBody(title = title, description = description, icon = icon) { - SpacerW12() - Icon( - painter = painterResource(id = R.drawable.ic_refresh_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } - }, - onClick = onClick, - ) -} - -// region elements - -@Composable -private fun WarningBody( - title: String, - description: String?, - iconBackground: Color = TangemTheme.colors.background.secondary, - icon: @Composable (() -> Unit)? = null, - additionalContent: @Composable () -> Unit = {}, -) { - IconWithTitleAndDescription( - title = title, - description = description, - additionalContent = additionalContent, - icon = icon ?: { - Image( - painter = painterResource(id = R.drawable.img_attention_20), - contentDescription = null, - ) - }, - iconBackground = iconBackground, - ) -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun WarningCardSurface(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) { - Card( - shape = RoundedCornerShape(TangemTheme.dimens.size12), - backgroundColor = TangemTheme.colors.background.primary, - elevation = TangemTheme.dimens.elevation2, - onClick = onClick ?: {}, - enabled = onClick != null, - ) { - content() - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun WarningCardMaterial3Style(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) { - Card( - shape = RoundedCornerShape(TangemTheme.dimens.radius16), - backgroundColor = TangemTheme.colors.button.disabled, - elevation = TangemTheme.dimens.elevation0, - onClick = onClick ?: {}, - enabled = onClick != null, - ) { - content() - } -} - -// endregion elements - -// region Preview - -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun WarningsPreview() { - TangemThemePreview { - Column(modifier = Modifier.fillMaxWidth()) { - WarningCard( - title = "Exchange rate has expired", - description = "To access all the networks, you need to scan the card.", - ) - SpacerH32() - ClickableWarningCard( - title = "Exchange rate has expired", - description = "To access all the networks, you need to scan the card.", - onClick = {}, - ) - SpacerH32() - RefreshableWarningCard( - title = "Exchange rate has expired", - description = "To access all the networks, you need to scan the card.", - onClick = {}, - ) - } - } -} -// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/pullToRefresh/PullToRefreshConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/PullToRefreshConfig.kt similarity index 84% rename from core/ui/src/main/java/com/tangem/core/ui/pullToRefresh/PullToRefreshConfig.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/PullToRefreshConfig.kt index b6c6aea4b9..d2fcfad70f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/pullToRefresh/PullToRefreshConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/PullToRefreshConfig.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.pullToRefresh +package com.tangem.core.ui.components.containers.pullToRefresh /** * Pull to refresh config data diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt new file mode 100644 index 0000000000..386f268756 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt @@ -0,0 +1,63 @@ +package com.tangem.core.ui.components.containers.pullToRefresh + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TangemPullToRefreshContainer( + config: PullToRefreshConfig, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val state = rememberPullToRefreshState() + PullToRefreshBox( + isRefreshing = config.isRefreshing, + onRefresh = { + config.onRefresh(PullToRefreshConfig.ShowRefreshState()) + }, + state = state, + modifier = modifier, + indicator = { + Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = config.isRefreshing, + state = state, + containerColor = TangemTheme.colors.background.tertiary, + color = TangemTheme.colors.text.primary1, + ) + }, + ) { + content() + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemPullToRefreshContainer_Preview() { + TangemThemePreview { + TangemPullToRefreshContainer( + config = PullToRefreshConfig(isRefreshing = true, {}), + ) { + Box( + modifier = Modifier.fillMaxSize() + .background(TangemTheme.colors.background.secondary), + ) + } + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt deleted file mode 100644 index 40e33e05ac..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ /dev/null @@ -1,206 +0,0 @@ -package com.tangem.core.ui.components.notifications - -import android.content.res.Configuration -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.material3.ButtonColors -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.ripple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import androidx.constraintlayout.compose.Visibility -import com.tangem.core.ui.R -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.res.LocalIsInDarkTheme -import com.tangem.core.ui.res.TangemColorPalette.Dark6 -import com.tangem.core.ui.res.TangemColorPalette.Light4 -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -/** - * Custom notification with image background - * @see [Swap Promo](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=8713-6307&mode=dev) - */ -@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries") -@Composable -fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = Modifier) { - val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig - - ConstraintLayout( - modifier = modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size62) - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .clickable( - enabled = config.onClick != null, - onClick = config.onClick ?: {}, - ), - ) { - val (iconRef, titleRef, subtitleRef, closeIconRef, buttonRef, backgroundRef) = createRefs() - - val spacing2 = TangemTheme.dimens.spacing2 - val spacing12 = TangemTheme.dimens.spacing12 - val spacing14 = TangemTheme.dimens.spacing14 - - Image( - painter = painterResource(config.backgroundResId ?: R.drawable.img_swap_promo_blue_banner_background), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.constrainAs(backgroundRef) { - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - bottom.linkTo(parent.bottom) - width = Dimension.fillToConstraints - height = Dimension.fillToConstraints - visibility = if (config.backgroundResId == null) Visibility.Gone else Visibility.Visible - }, - ) - Image( - painter = painterResource(config.iconResId), - contentDescription = null, - modifier = Modifier - .size(TangemTheme.dimens.size34) - .constrainAs(iconRef) { - start.linkTo(parent.start, spacing12) - linkTo(titleRef.top, subtitleRef.bottom, bias = 0.1f) - }, - ) - - val titleText = config.title?.resolveReference() - if (titleText != null) { - Text( - text = titleText, - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.constantWhite, - modifier = Modifier.constrainAs(titleRef) { - top.linkTo(parent.top, spacing12) - start.linkTo(iconRef.end, spacing12) - end.linkTo(closeIconRef.start, spacing2) - width = Dimension.fillToConstraints - }, - ) - } - - Text( - text = config.subtitle.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.constantWhite, - modifier = Modifier.constrainAs(subtitleRef) { - top.linkTo(titleRef.bottom, spacing2) - start.linkTo(iconRef.end, spacing12) - end.linkTo(parent.end, spacing12) - bottom.linkTo(buttonRef.top, spacing12, spacing14) - width = Dimension.fillToConstraints - }, - ) - Icon( - painter = painterResource(id = R.drawable.ic_close_24), - contentDescription = null, - tint = TangemTheme.colors.text.constantWhite, - modifier = Modifier - .size(TangemTheme.dimens.size16) - .constrainAs(closeIconRef) { - top.linkTo(parent.top, spacing12) - end.linkTo(parent.end, spacing12) - } - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = false), - ) { - config.onCloseClick?.invoke() - }, - ) - val isDarkMode = LocalIsInDarkTheme.current - TangemButton( - modifier = Modifier.constrainAs(buttonRef) { - start.linkTo(parent.start, spacing12) - end.linkTo(parent.end, spacing12) - bottom.linkTo(parent.bottom, spacing12) - width = Dimension.fillToConstraints - visibility = if (button == null) { - Visibility.Gone - } else { - Visibility.Visible - } - }, - text = button?.text?.resolveReference().orEmpty(), - icon = TangemButtonIconPosition.Start(button?.iconResId ?: R.drawable.ic_exchange_vertical_24), - onClick = button?.onClick ?: {}, - colors = ButtonColors( - containerColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary, - contentColor = Dark6, - disabledContainerColor = TangemTheme.colors.button.disabled, - disabledContentColor = TangemTheme.colors.text.disabled, - ), - textStyle = TangemTheme.typography.subtitle1, - enabled = true, - showProgress = false, - ) - } -} - -//region preview -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun NotificationWithBackgroundPreview( - @PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig, -) { - TangemThemePreview { - NotificationWithBackground(config = config) - } -} - -private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - NotificationConfig( - title = resourceReference(id = R.string.main_swap_changelly_promotion_title), - subtitle = resourceReference( - id = R.string.main_swap_changelly_promotion_message, - formatArgs = wrappedList("1", "2", "3"), - ), - iconResId = R.drawable.img_swap_promo, - backgroundResId = R.drawable.img_swap_promo_blue_banner_background, - ), - NotificationConfig( - title = resourceReference( - id = R.string.token_swap_changelly_promotion_title, - formatArgs = wrappedList("1"), - ), - subtitle = stringReference( - "Swap multiple currencies between any chains you wish. Swap multiple " + - "currencies between any chains you wish. Swap multiple " + - "currencies between any chains you wish. Commission free period " + - "till Dec 31.", - ), - iconResId = R.drawable.img_swap_promo, - backgroundResId = R.drawable.img_swap_promo_blue_banner_background, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(id = R.string.token_swap_promotion_button), - onClick = {}, - ), - ), - ) -} -//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt deleted file mode 100644 index beadc7ce9e..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.tangem.core.ui.components.notifications - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.material3.ButtonColors -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.ripple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.core.ui.R -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.LocalIsInDarkTheme -import com.tangem.core.ui.res.TangemColorPalette.Dark6 -import com.tangem.core.ui.res.TangemColorPalette.Light4 -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -private val OxkPromoColor = Color(0xFFBCFF2F) - -@Composable -fun OkxPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) { - Column( - modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(Dark6), - ) { - Content(config = config) - Button(config = config) - } -} - -@Composable -private fun Content(config: NotificationConfig) { - Row { - Icon( - painter = rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.img_okx_dex_logo)), - contentDescription = null, - tint = TangemTheme.colors.icon.constant, - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing12) - .align(Alignment.CenterVertically), - ) - Column( - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - modifier = Modifier - .weight(1f) - .padding(TangemTheme.dimens.spacing12), - ) { - val titleText = config.title?.resolveReference() - - if (titleText != null) { - Text( - text = titleText, - style = TangemTheme.typography.button, - color = OxkPromoColor, - ) - } - - Text( - text = config.subtitle.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.constantWhite, - ) - } - config.onCloseClick?.let { - Icon( - painter = rememberVectorPainter( - image = ImageVector.vectorResource(R.drawable.ic_close_24), - ), - contentDescription = null, - tint = TangemTheme.colors.icon.constant, - modifier = - Modifier - .padding( - top = TangemTheme.dimens.spacing8, - end = TangemTheme.dimens.spacing8, - ) - .size(TangemTheme.dimens.size20) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(radius = TangemTheme.dimens.radius10), - onClick = it, - ), - ) - } - } -} - -@Composable -private fun Button(config: NotificationConfig) { - val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig - - button?.let { - val isDarkMode = LocalIsInDarkTheme.current - TangemButton( - modifier = Modifier - .fillMaxWidth() - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), - text = button.text.resolveReference(), - icon = TangemButtonIconPosition.Start(button.iconResId ?: R.drawable.ic_exchange_vertical_24), - onClick = button.onClick, - colors = ButtonColors( - containerColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary, - contentColor = Dark6, - disabledContainerColor = TangemTheme.colors.button.disabled, - disabledContentColor = TangemTheme.colors.text.disabled, - ), - textStyle = TangemTheme.typography.subtitle1, - enabled = true, - showProgress = false, - ) - } -} - -// region Preview -@Preview(widthDp = 360) -@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun OkxPromoNotification_Preview( - @PreviewParameter(OkxPromoNotificationPreviewProvider::class) data: NotificationConfig, -) { - TangemThemePreview { - OkxPromoNotification(data) - } -} - -private class OkxPromoNotificationPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - NotificationConfig( - title = resourceReference(R.string.swap_promo_title), - subtitle = resourceReference(R.string.swap_promo_text), - iconResId = R.drawable.img_okx_dex_logo, - onCloseClick = {}, - ), - NotificationConfig( - title = resourceReference(R.string.swap_promo_title), - subtitle = resourceReference(R.string.swap_promo_text), - iconResId = R.drawable.img_okx_dex_logo, - onCloseClick = {}, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.token_swap_promotion_button), - onClick = {}, - ), - ), - ) -} -// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/RingPromoNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/RingPromoNotification.kt deleted file mode 100644 index c40947749d..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/RingPromoNotification.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.tangem.core.ui.components.notifications - -import android.content.res.Configuration -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.FixedScale -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemColorPalette -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -@Composable -fun RingPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) { - var textHeightDp by remember { mutableStateOf(0.dp) } - - Box( - modifier = modifier - .fillMaxWidth() - .clip(shape = TangemTheme.shapes.roundedCornersXMedium) - .background(TangemColorPalette.Dark6), - ) { - PromoImage( - backgroundRes = config.backgroundResId, - iconRes = config.iconResId, - modifier = Modifier.height(textHeightDp), - ) - PromoText( - title = config.title, - subtitle = config.subtitle, - onSizeChange = { textHeightDp = it }, - ) - CloseableIconButton( - onClick = config.onCloseClick, - modifier = Modifier.align(alignment = Alignment.TopEnd), - iconTint = TangemTheme.colors.icon.informative, - ) - } -} - -@Composable -private fun PromoImage(@DrawableRes backgroundRes: Int?, @DrawableRes iconRes: Int, modifier: Modifier = Modifier) { - if (backgroundRes == null) return - ConstraintLayout( - modifier = modifier, - ) { - val (background, image) = createRefs() - - Image( - painter = painterResource(backgroundRes), - contentDescription = null, - contentScale = FixedScale(2f), - modifier = Modifier - .constrainAs(background) { - top.linkTo(parent.top) - start.linkTo(image.start) - end.linkTo(image.end) - }, - ) - Image( - painter = painterResource(id = iconRes), - contentDescription = null, - contentScale = ContentScale.FillWidth, - modifier = Modifier - .requiredWidth(width = 60.dp) - .wrapContentHeight(Alignment.Top, unbounded = true) - .constrainAs(image) { - top.linkTo(parent.top, 11.dp) - start.linkTo(parent.start, 9.dp) - }, - ) - } -} - -@Composable -private fun PromoText(title: TextReference?, subtitle: TextReference, onSizeChange: (Dp) -> Unit) { - val density = LocalDensity.current - - Box( - modifier = Modifier.onSizeChanged { - with(density) { onSizeChange(it.height.toDp()) } - }, - ) { - TextsBlock( - title = title, - subtitle = subtitle, - subtitleColor = TangemTheme.colors.text.tertiary, - titleColor = TangemTheme.colors.text.constantWhite, - modifier = Modifier - .wrapContentHeight() - .align(Alignment.CenterStart) - .padding(start = 80.dp, top = 12.dp, end = 12.dp, bottom = 12.dp), - ) - } -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, locale = "fr") -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun RingPromoNotification_Preview() { - TangemThemePreview { - RingPromoNotification( - NotificationConfig( - title = resourceReference(id = R.string.ring_promo_title), - subtitle = resourceReference(id = R.string.ring_promo_text), - iconResId = R.drawable.img_card_with_ring, - backgroundResId = R.drawable.img_ring_promo_background, - onCloseClick = { }, - ), - ) - } -} - -// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/TangemLinearProgressIndicator.kt similarity index 51% rename from core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/progressbar/TangemLinearProgressIndicator.kt index 05cbf5db85..e9fddfa79e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/LinearProgressIndicator.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/progressbar/TangemLinearProgressIndicator.kt @@ -3,11 +3,11 @@ package com.tangem.core.ui.components.progressbar import android.content.res.Configuration +import androidx.compose.animation.core.* import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -39,7 +39,7 @@ import kotlin.math.abs * @param strokeCap stroke cap to use for the ends of this progress indicator */ @Composable -fun LinearProgressIndicator( +fun TangemLinearProgressIndicator( progress: () -> Float, modifier: Modifier = Modifier, color: Color = TangemTheme.colors.text.accent, @@ -61,6 +61,99 @@ fun LinearProgressIndicator( } } +/** + * Progress indicators loading status with infinite animation + * + * @param modifier the [Modifier] to be applied to this progress indicator + * @param color The color of the progress indicator. + * @param backgroundColor The color of the background behind the indicator, visible when the + * progress has not reached that area of the overall indicator yet. + * @param strokeCap stroke cap to use for the ends of this progress indicator + */ +@Suppress("LongMethod") +@Composable +fun TangemLinearProgressIndicator( + modifier: Modifier = Modifier, + color: Color = TangemTheme.colors.text.accent, + backgroundColor: Color = TangemTheme.colors.background.tertiary, + strokeCap: StrokeCap = StrokeCap.Round, +) { + val infiniteTransition = rememberInfiniteTransition() + val firstLineHead by infiniteTransition.animateFloat( + 0f, + 1f, + infiniteRepeatable( + animation = keyframes { + durationMillis = LINEAR_ANIMATION_DURATION + 0f at FIRST_LINE_HEAD_DELAY using FIRST_LINE_HEAD_EASING + 1f at FIRST_LINE_HEAD_DURATION + FIRST_LINE_HEAD_DELAY + }, + ), + ) + val firstLineTail by infiniteTransition.animateFloat( + 0f, + 1f, + infiniteRepeatable( + animation = keyframes { + durationMillis = LINEAR_ANIMATION_DURATION + 0f at FIRST_LINE_TAIL_DELAY using FIRST_LINE_TAIL_EASING + 1f at FIRST_LINE_TAIL_DURATION + FIRST_LINE_TAIL_DELAY + }, + ), + ) + val secondLineHead by infiniteTransition.animateFloat( + 0f, + 1f, + infiniteRepeatable( + animation = keyframes { + durationMillis = LINEAR_ANIMATION_DURATION + 0f at SECOND_LINE_HEAD_DELAY using SECOND_LINE_HEAD_EASING + 1f at SECOND_LINE_HEAD_DURATION + SECOND_LINE_HEAD_DELAY + }, + ), + ) + val secondLineTail by infiniteTransition.animateFloat( + 0f, + 1f, + infiniteRepeatable( + animation = keyframes { + durationMillis = LINEAR_ANIMATION_DURATION + 0f at SECOND_LINE_TAIL_DELAY using SECOND_LINE_TAIL_EASING + 1f at SECOND_LINE_TAIL_DURATION + SECOND_LINE_TAIL_DELAY + }, + ), + ) + Canvas( + modifier + .increaseSemanticsBounds() + .semantics(mergeDescendants = true) { + progressBarRangeInfo = ProgressBarRangeInfo.Indeterminate + } + .size(height = 4.dp, width = 24.dp), + ) { + val strokeWidth = size.height + drawLinearIndicatorBackground(backgroundColor, strokeWidth, strokeCap) + if (firstLineHead - firstLineTail > 0) { + drawLinearIndicator( + firstLineHead, + firstLineTail, + color, + strokeWidth, + strokeCap, + ) + } + if ((secondLineHead - secondLineTail) > 0) { + drawLinearIndicator( + secondLineHead, + secondLineTail, + color, + strokeWidth, + strokeCap, + ) + } + } +} + internal fun Modifier.increaseSemanticsBounds(): Modifier { val padding = 10.dp return this @@ -129,16 +222,51 @@ private fun DrawScope.drawLinearIndicator( private fun DrawScope.drawLinearIndicatorBackground(color: Color, strokeWidth: Float, strokeCap: StrokeCap) = drawLinearIndicator(0f, 1f, color, strokeWidth, strokeCap) +// Indeterminate linear indicator transition specs +// Total duration for one cycle +private const val LINEAR_ANIMATION_DURATION = 1800 + +// Duration of the head and tail animations for both lines +private const val FIRST_LINE_HEAD_DURATION = 750 +private const val FIRST_LINE_TAIL_DURATION = 850 +private const val SECOND_LINE_HEAD_DURATION = 567 +private const val SECOND_LINE_TAIL_DURATION = 533 + +// Delay before the start of the head and tail animations for both lines +private const val FIRST_LINE_HEAD_DELAY = 0 +private const val FIRST_LINE_TAIL_DELAY = 333 +private const val SECOND_LINE_HEAD_DELAY = 1000 +private const val SECOND_LINE_TAIL_DELAY = 1267 + +private val FIRST_LINE_HEAD_EASING = CubicBezierEasing(0.2f, 0f, 0.8f, 1f) +private val FIRST_LINE_TAIL_EASING = CubicBezierEasing(0.4f, 0f, 1f, 1f) +private val SECOND_LINE_HEAD_EASING = CubicBezierEasing(0f, 0f, 0.65f, 1f) +private val SECOND_LINE_TAIL_EASING = CubicBezierEasing(0.1f, 0f, 0.45f, 1f) + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun LinearProgressIndicator_Preview() { TangemThemePreview { - LinearProgressIndicator( - progress = { 0.5f }, - modifier = Modifier.fillMaxWidth(), - ) + Column( + modifier = Modifier + .background(TangemTheme.colors.background.secondary) + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + TangemLinearProgressIndicator( + progress = { 0.6f }, + modifier = Modifier.fillMaxWidth(), + color = TangemTheme.colors.icon.primary1, + backgroundColor = TangemTheme.colors.background.tertiary, + ) + TangemLinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = TangemTheme.colors.icon.primary1, + backgroundColor = TangemTheme.colors.background.tertiary, + ) + } } } // endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index e89fd74ee3..c0571b8065 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -87,6 +87,8 @@ fun getActiveIconRes(blockchainId: String): Int { "fact0rn" -> R.drawable.img_fact0rn_22 "odyssey", "odyssey/test" -> R.drawable.img_odyssey_chain_22 "bitrock", "bitrock/test" -> R.drawable.img_bitrock_22 + "sonic", "sonic/test" -> R.drawable.img_sonic_22 + "apechain", "apechain/test" -> R.drawable.img_apecoin_22 else -> R.drawable.ic_alert_24 } } @@ -172,6 +174,8 @@ fun getActiveIconResByCoinId(coinId: String): Int { "fact0rn" -> R.drawable.img_fact0rn_22 "odyssey", "odyssey/test" -> R.drawable.img_odyssey_chain_22 "bitrock", "bitrock/test" -> R.drawable.img_bitrock_22 + "sonic", "sonic/test" -> R.drawable.img_sonic_22 + "apechain", "apechain/test" -> R.drawable.img_apecoin_22 else -> R.drawable.ic_alert_24 } } @@ -260,6 +264,8 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "fact0rn" -> R.drawable.ic_fact0rn_22 "odyssey", "odyssey/test" -> R.drawable.ic_odyssey_chain_22 "bitrock", "bitrock/test" -> R.drawable.ic_bitrock_22 + "sonic", "sonic/test" -> R.drawable.ic_sonic_22 + "apechain", "apechain/test" -> R.drawable.ic_apecoin_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/OutsetBorder.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/OutsetBorder.kt new file mode 100644 index 0000000000..8dfc3ecd52 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/OutsetBorder.kt @@ -0,0 +1,42 @@ +package com.tangem.core.ui.extensions + +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.isSimple +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.Dp + +/** + * Modifier that adds an outset border to the content. + * @see [androidx.compose.foundation.border] + * + * @param width The width of the border. + * @param color The color of the border. + * @param shape The shape of the border. + */ +fun Modifier.outsetBorder(width: Dp, color: Color, shape: Shape) = drawWithCache { + val outline = shape.createOutline(size, layoutDirection, this) + val cornerRadius = when { + outline is Outline.Rounded && outline.roundRect.isSimple -> outline.roundRect.topLeftCornerRadius + else -> CornerRadius.Zero + } + + onDrawWithContent { + drawContent() + drawRoundRect( + color = color, + topLeft = Offset(-width.toPx() / 2, -width.toPx() / 2), + size = size.copy( + width = size.width + width.toPx(), + height = size.height + width.toPx(), + ), + cornerRadius = cornerRadius, + style = Stroke(width = width.toPx()), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/security/DisableScreenshots.kt b/core/ui/src/main/java/com/tangem/core/ui/security/DisableScreenshots.kt new file mode 100644 index 0000000000..4a421ead77 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/security/DisableScreenshots.kt @@ -0,0 +1,24 @@ +package com.tangem.core.ui.security + +import android.view.WindowManager +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalContext +import com.tangem.core.ui.utils.findActivity + +/** + * Disables screenshots for the current composition. + */ +@Composable +fun DisableScreenshotsDisposableEffect() { + val activity = LocalContext.current.findActivity() + DisposableEffect(activity) { + activity.window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE, + ) + onDispose { + activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/ContextUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/ContextUtils.kt new file mode 100644 index 0000000000..576cf1f08a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/ContextUtils.kt @@ -0,0 +1,14 @@ +package com.tangem.core.ui.utils + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper + +fun Context.findActivity(): Activity { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + error("Cannot find activity. Please make sure that the context is attached to an activity.") +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_apecoin_22.xml b/core/ui/src/main/res/drawable/ic_apecoin_22.xml new file mode 100644 index 0000000000..99e14d7f9c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_apecoin_22.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_loader.xml b/core/ui/src/main/res/drawable/ic_loader.xml new file mode 100644 index 0000000000..5c61f9a912 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_loader.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_sonic_22.xml b/core/ui/src/main/res/drawable/ic_sonic_22.xml new file mode 100644 index 0000000000..c4a026c93a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_sonic_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_tangem_card_grey.xml b/core/ui/src/main/res/drawable/ic_tangem_card_grey.xml new file mode 100644 index 0000000000..fb390ee999 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tangem_card_grey.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_wallet_filled_24.xml b/core/ui/src/main/res/drawable/ic_wallet_filled_24.xml new file mode 100644 index 0000000000..c2e83ec569 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_wallet_filled_24.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_apecoin_22.xml b/core/ui/src/main/res/drawable/img_apecoin_22.xml new file mode 100644 index 0000000000..8a9a5266a0 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_apecoin_22.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_card_visa.webp b/core/ui/src/main/res/drawable/img_card_visa.webp index bd2804ca99..f51b3d8f95 100644 Binary files a/core/ui/src/main/res/drawable/img_card_visa.webp and b/core/ui/src/main/res/drawable/img_card_visa.webp differ diff --git a/core/ui/src/main/res/drawable/img_ring_promo_background.webp b/core/ui/src/main/res/drawable/img_ring_promo_background.webp deleted file mode 100644 index aa5ae8be39..0000000000 Binary files a/core/ui/src/main/res/drawable/img_ring_promo_background.webp and /dev/null differ diff --git a/core/ui/src/main/res/drawable/img_sonic_22.xml b/core/ui/src/main/res/drawable/img_sonic_22.xml new file mode 100644 index 0000000000..22ab5bd01e --- /dev/null +++ b/core/ui/src/main/res/drawable/img_sonic_22.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_swap_promo.webp b/core/ui/src/main/res/drawable/img_swap_promo.webp deleted file mode 100644 index 4f5bd02d24..0000000000 Binary files a/core/ui/src/main/res/drawable/img_swap_promo.webp and /dev/null differ diff --git a/core/ui/src/main/res/drawable/img_swap_promo_blue_banner_background.webp b/core/ui/src/main/res/drawable/img_swap_promo_blue_banner_background.webp deleted file mode 100644 index 1e952cd6f7..0000000000 Binary files a/core/ui/src/main/res/drawable/img_swap_promo_blue_banner_background.webp and /dev/null differ diff --git a/core/ui/src/main/res/drawable/img_swap_promo_green_banner_background.webp b/core/ui/src/main/res/drawable/img_swap_promo_green_banner_background.webp deleted file mode 100644 index ffaced2ad2..0000000000 Binary files a/core/ui/src/main/res/drawable/img_swap_promo_green_banner_background.webp and /dev/null differ diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt index cb9495d708..e4006b694a 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt @@ -26,6 +26,16 @@ internal class DefaultCardSdkConfigRepository( override val sdk: TangemSdk get() = cardSdkProvider.sdk + override var isBiometricsRequestPolicy: Boolean + get() = sdk.config.userCodeRequestPolicy is UserCodeRequestPolicy.AlwaysWithBiometrics + set(value) { + sdk.config.userCodeRequestPolicy = if (value) { + UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode) + } else { + UserCodeRequestPolicy.Default + } + } + override fun setAccessCodeRequestPolicy(isBiometricsRequestPolicy: Boolean) { sdk.config.userCodeRequestPolicy = if (isBiometricsRequestPolicy) { UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt index 7b9c905f0d..35efb1a541 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt @@ -18,7 +18,7 @@ class CryptoCurrencyFactory( @Suppress("LongParameterList") // Yep, it's long fun createToken( network: Network, - rawId: String?, + rawId: CryptoCurrency.RawID?, name: String, symbol: String, decimals: Int, @@ -125,7 +125,7 @@ class CryptoCurrencyFactory( symbol = cryptoCurrency.symbol, contractAddress = cryptoCurrency.contractAddress, decimals = cryptoCurrency.decimals, - id = cryptoCurrency.id.rawCurrencyId, + id = cryptoCurrency.id.rawCurrencyId?.value, ) val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown val id = getTokenId(network, sdkToken) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt index b27a7f17ef..ccf75203ca 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt @@ -294,6 +294,8 @@ private fun Blockchain.getSupportedTransactionExtras(): Network.TransactionExtra Blockchain.VanarChainTestnet, Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet, Blockchain.Bitrock, Blockchain.BitrockTestnet, + Blockchain.Sonic, Blockchain.SonicTestnet, + Blockchain.ApeChain, Blockchain.ApeChainTestnet, -> Network.TransactionExtrasType.NONE // endregion } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt index 724649c959..e2cfb5809b 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/TokensOperations.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency.ID import com.tangem.domain.tokens.model.Network import com.tangem.blockchain.common.Token as SdkToken @@ -31,15 +32,16 @@ fun getCoinId(network: Network, coinId: String): ID { fun getTokenId(network: Network, sdkToken: SdkToken): ID { val sdkTokenId = sdkToken.id + val tokenId = sdkTokenId?.let { CryptoCurrency.RawID(it) } - return getTokenId(network, sdkTokenId, sdkToken.contractAddress) + return getTokenId(network, tokenId, sdkToken.contractAddress) } -fun getTokenId(network: Network, rawTokenId: String?, contractAddress: String): ID { +fun getTokenId(network: Network, rawTokenId: CryptoCurrency.RawID?, contractAddress: String): ID { val suffix = if (rawTokenId == null) { CustomCurrencyIdSuffix(contractAddress) } else { - CurrencyIdSuffix(rawTokenId, contractAddress) + CurrencyIdSuffix(rawTokenId.value, contractAddress) } return ID(TOKEN_ID_PREFIX, getCurrencyIdBody(network), suffix) @@ -47,11 +49,12 @@ fun getTokenId(network: Network, rawTokenId: String?, contractAddress: String): fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { val tokenId = token.id + val rawID = tokenId?.let { CryptoCurrency.RawID(it) } - return if (tokenId == null) { + return if (rawID == null) { IconsUtil.getTokenIconUri(blockchain, token)?.toString() } else { - getTokenIconUrlFromDefaultHost(tokenId) + getTokenIconUrlFromDefaultHost(rawID) } } @@ -59,7 +62,7 @@ fun getCoinIconUrl(blockchain: Blockchain): String? { val coinId = when (blockchain) { Blockchain.Unknown -> null else -> blockchain.toCoinId() - } + }?.let { CryptoCurrency.RawID(it) } return coinId?.let(::getTokenIconUrlFromDefaultHost) } @@ -84,13 +87,13 @@ private fun getCurrencyIdBody(network: Network): CurrencyIdBody { } } -fun getTokenIconUrlFromDefaultHost(tokenId: String): String { +fun getTokenIconUrlFromDefaultHost(tokenId: CryptoCurrency.RawID): String { return buildString { append(DEFAULT_TOKENS_ICONS_HOST) append('/') append(TOKEN_ICON_SIZE) append('/') - append(tokenId) + append(tokenId.value) append('.') append(TOKEN_ICON_EXT) } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt index 4ba8bbd2a2..b01f5114ad 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt @@ -30,7 +30,7 @@ class UserTokensResponseFactory { val blockchain = getBlockchain(currency.network.id) return UserTokensResponse.Token( - id = currency.id.rawCurrencyId, + id = currency.id.rawCurrencyId?.value, networkId = blockchain.toNetworkId(), derivationPath = currency.network.derivationPath.value, name = currency.name, diff --git a/data/manage-tokens/build.gradle.kts b/data/manage-tokens/build.gradle.kts index d2ff6fbc92..a8373e38ff 100644 --- a/data/manage-tokens/build.gradle.kts +++ b/data/manage-tokens/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -44,4 +46,6 @@ dependencies { /** Other */ implementation(deps.moshi.kotlin) implementation(deps.timber) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index e146654abd..6c9f005ae9 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -122,7 +122,7 @@ internal class DefaultCustomTokensRepository( if (coinNetwork != null) { cryptoCurrencyFactory.createToken( network = network, - rawId = coin.id, + rawId = CryptoCurrency.RawID(coin.id), name = coin.name, symbol = coin.symbol, decimals = coinNetwork.decimalCount!!.toInt(), @@ -154,7 +154,7 @@ internal class DefaultCustomTokensRepository( override suspend fun createToken( managedCryptoCurrency: ManagedCryptoCurrency.Token, sourceNetwork: ManagedCryptoCurrency.SourceNetwork.Default, - rawId: String?, + rawId: CryptoCurrency.RawID?, ): CryptoCurrency.Token { return cryptoCurrencyFactory.createToken( network = sourceNetwork.network, diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index 5ac35825b0..98ccc3e720 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -20,6 +20,7 @@ import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network internal class ManagedCryptoCurrencyFactory( @@ -114,7 +115,11 @@ internal class ManagedCryptoCurrencyFactory( ) } else { ManagedCryptoCurrency.Custom.Token( - currencyId = getTokenId(network, token.id, contractAddress), + currencyId = getTokenId( + network = network, + rawTokenId = token.id?.let { CryptoCurrency.RawID(it) }, + contractAddress = contractAddress, + ), name = token.name, symbol = token.symbol, iconUrl = token.id?.let { getIconUrl(it) }, diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index 632a570f78..9e8f4d07a7 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -22,6 +24,7 @@ dependencies { implementation(projects.domain.markets) implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.tokens) implementation(projects.data.common) @@ -39,5 +42,7 @@ dependencies { implementation(deps.moshi.kotlin) implementation(deps.timber) implementation(deps.tangem.blockchain) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion } diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 4159ad1c21..4b2f305bcf 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -125,10 +125,10 @@ internal class DefaultMarketsTokenRepository( override suspend fun getChart( fiatCurrencyCode: String, interval: PriceChangeInterval, - tokenId: String, + tokenId: CryptoCurrency.RawID, tokenSymbol: String, ) = withContext(dispatcherProvider.io) { - val mappedTokenId = getTokenIdIfL2Network(tokenId) + val mappedTokenId = getTokenIdIfL2Network(tokenId.value) val response = marketsApi.getCoinChart( currency = fiatCurrencyCode, coinId = mappedTokenId, @@ -161,10 +161,10 @@ internal class DefaultMarketsTokenRepository( override suspend fun getChartPreview( fiatCurrencyCode: String, interval: PriceChangeInterval, - tokenId: String, + tokenId: CryptoCurrency.RawID, tokenSymbol: String, ) = withContext(dispatcherProvider.io) { - val mappedTokenId = getTokenIdIfL2Network(tokenId) + val mappedTokenId = getTokenIdIfL2Network(tokenId.value) val chart = catchListErrorAndSendEvent { marketsApi.getCoinsListCharts( @@ -193,7 +193,7 @@ internal class DefaultMarketsTokenRepository( override suspend fun getTokenInfo( fiatCurrencyCode: String, - tokenId: String, + tokenId: CryptoCurrency.RawID, tokenSymbol: String, languageCode: String, ) = withContext(dispatcherProvider.io) { @@ -203,16 +203,16 @@ internal class DefaultMarketsTokenRepository( ) { marketsApi.getCoinMarketData( currency = fiatCurrencyCode, - coinId = tokenId, + coinId = tokenId.value, language = languageCode, ).getOrThrow() } - val resultResponse = result.applyL2Compatibility(tokenId) + val resultResponse = result.applyL2Compatibility(tokenId.value) return@withContext tokenMarketInfoConverter.convert(resultResponse) } - override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String, tokenSymbol: String) = + override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: CryptoCurrency.RawID, tokenSymbol: String) = withContext(dispatcherProvider.io) { // for second markets iteration we should use extended api method with all required fields @@ -222,7 +222,7 @@ internal class DefaultMarketsTokenRepository( ) { tangemTechApi.getQuotes( currencyId = fiatCurrencyCode, - coinIds = tokenId, + coinIds = tokenId.value, fields = marketsQuoteFields.joinToString(separator = ","), ).getOrThrow() } @@ -263,10 +263,10 @@ internal class DefaultMarketsTokenRepository( } } - override suspend fun getTokenExchanges(tokenId: String): List { + override suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List { return withContext(dispatcherProvider.io) { cacheRegistry.invokeOnExpire(key = "coins/$tokenId/exchanges", skipCache = false) { - val response = marketsApi.getCoinExchanges(coinId = tokenId).getOrThrow() + val response = marketsApi.getCoinExchanges(coinId = tokenId.value).getOrThrow() tokenExchangesStore.store(value = response.exchanges) } diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt index fdd44ffba9..e426157da0 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt @@ -4,16 +4,17 @@ import com.tangem.datasource.api.markets.models.response.TokenMarketChartListRes import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenMarketListConfig +import com.tangem.domain.tokens.model.CryptoCurrency internal object TokenMarketChartsConverter { fun convert( chartsToCopy: TokenMarket.Charts, - tokenId: String, + tokenId: CryptoCurrency.RawID, interval: TokenMarketListConfig.Interval, value: TokenMarketChartListResponse, ): TokenMarket.Charts { - val prices = requireNotNull(value[tokenId]) { + val prices = requireNotNull(value[tokenId.value]) { "$tokenId is not found in the response. This shouldn't have happened." } return when (interval) { diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index c4836449f3..38d39f1843 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -3,6 +3,7 @@ package com.tangem.data.markets.converters import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenQuotesShort +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.converter.Converter internal object TokenMarketListConverter : Converter> { @@ -18,7 +19,7 @@ internal object TokenMarketListConverter : Converter TokenMarket( - id = token.id, + id = CryptoCurrency.RawID(token.id), name = token.name, symbol = token.symbol, marketRating = token.marketRating, diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesShortConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesShortConverter.kt index 2bc1c6933c..c3c78e5dfb 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesShortConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesShortConverter.kt @@ -2,12 +2,13 @@ package com.tangem.data.markets.converters import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.domain.markets.TokenQuotesShort +import com.tangem.domain.tokens.model.CryptoCurrency import java.math.BigDecimal internal object TokenQuotesShortConverter { - fun convert(tokenId: String, value: QuotesResponse): TokenQuotesShort { - val quote = requireNotNull(value.quotes[tokenId]) { + fun convert(tokenId: CryptoCurrency.RawID, value: QuotesResponse): TokenQuotesShort { + val quote = requireNotNull(value.quotes[tokenId.value]) { "$tokenId is not found in the response. This shouldn't have happened." } return TokenQuotesShort( diff --git a/data/onramp/build.gradle.kts b/data/onramp/build.gradle.kts index 8d28d658eb..dd8b614228 100644 --- a/data/onramp/build.gradle.kts +++ b/data/onramp/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -24,6 +26,7 @@ dependencies { implementation(projects.domain.onramp) implementation(projects.domain.legacy) implementation(projects.domain.appTheme.models) + implementation(projects.domain.models) // region DI @@ -41,6 +44,10 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.timber) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) + implementation(deps.kotlin.serialization) + implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt new file mode 100644 index 0000000000..5e296aa7dd --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -0,0 +1,41 @@ +package com.tangem.data.onramp + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter +import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.onramp.model.HotCryptoCurrency +import com.tangem.domain.onramp.repositories.HotCryptoRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * Default implementation of [HotCryptoRepository] + * + * @property excludedBlockchains excluded blockchains + * @property hotCryptoResponseStore store of `HotCryptoResponse` + * @property userWalletsStore store of `UserWallet` + * +[REDACTED_AUTHOR] + */ +internal class DefaultHotCryptoRepository( + private val excludedBlockchains: ExcludedBlockchains, + private val hotCryptoResponseStore: HotCryptoResponseStore, + private val userWalletsStore: UserWalletsStore, +) : HotCryptoRepository { + + override fun getCurrencies(userWalletId: UserWalletId): Flow> { + return hotCryptoResponseStore.get() + .map { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) + ?: error("UserWalletId [$userWalletId] not found") + + HotCryptoCurrencyConverter( + scanResponse = userWallet.scanResponse, + imageHost = it.imageHost, + excludedBlockchains = excludedBlockchains, + ).convertList(input = it.tokens) + } + } +} \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt new file mode 100644 index 0000000000..884f659b20 --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt @@ -0,0 +1,78 @@ +package com.tangem.data.onramp.converters + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.getNetwork +import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.onramp.model.HotCryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.utils.converter.Converter + +/** + * Converter from [HotCryptoResponse.Token] to [HotCryptoCurrency] + * + * @property scanResponse scan response + * @property imageHost image host + * @property excludedBlockchains excluded blockchains + * +[REDACTED_AUTHOR] + */ +internal class HotCryptoCurrencyConverter( + private val scanResponse: ScanResponse, + private val imageHost: String?, + private val excludedBlockchains: ExcludedBlockchains, +) : Converter { + + private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) } + + override fun convert(value: HotCryptoResponse.Token): HotCryptoCurrency { + val network = createNetwork(value.networkId) + + val contractAddress = value.contractAddress + val decimals = value.decimalCount + val currency = if (contractAddress != null && decimals != null) { + cryptoCurrencyFactory.createToken( + network = network, + rawId = CryptoCurrency.RawID(value.id), + name = value.name, + symbol = value.symbol, + decimals = decimals, + contractAddress = contractAddress, + ) + } else { + cryptoCurrencyFactory.createCoin(network = network) + } + + return HotCryptoCurrency( + cryptoCurrency = currency.setupIconUrl(id = value.id), + fiatRate = value.currentPrice, + priceChange = value.priceChangePercentage, + ) + } + + private fun createNetwork(networkId: String): Network { + return requireNotNull( + getNetwork( + blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)), + extraDerivationPath = null, + scanResponse = scanResponse, + excludedBlockchains = excludedBlockchains, + ), + ) + } + + private fun CryptoCurrency.setupIconUrl(id: String): CryptoCurrency { + imageHost ?: return this + + val iconUrl = "${imageHost}large/$id.png" + + return when (this) { + is CryptoCurrency.Coin -> copy(iconUrl = iconUrl) + is CryptoCurrency.Token -> copy(iconUrl = iconUrl) + } + } +} \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index febd5c21c9..979d18d4b7 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -1,6 +1,8 @@ package com.tangem.data.onramp.di import com.squareup.moshi.Moshi +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.onramp.DefaultHotCryptoRepository import com.tangem.data.onramp.DefaultOnrampErrorResolver import com.tangem.data.onramp.DefaultOnrampRepository import com.tangem.data.onramp.DefaultOnrampTransactionRepository @@ -10,12 +12,15 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.onramp.repositories.HotCryptoRepository import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository @@ -83,4 +88,18 @@ internal object OnrampDataModule { OnrampErrorConverter(jsonAdapter), ) } + + @Provides + @Singleton + fun provideHotCryptoRepository( + excludedBlockchains: ExcludedBlockchains, + hotCryptoResponseStore: HotCryptoResponseStore, + userWalletsStore: UserWalletsStore, + ): HotCryptoRepository { + return DefaultHotCryptoRepository( + excludedBlockchains = excludedBlockchains, + hotCryptoResponseStore = hotCryptoResponseStore, + userWalletsStore = userWalletsStore, + ) + } } \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index 6a0307943f..c481880b51 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -1,94 +1,32 @@ package com.tangem.data.promo -import com.tangem.data.promo.converters.PromoResponseConverter -import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY -import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY -import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY -import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SWAP_STORIES_KEY import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.promo.PromoRepository -import com.tangem.domain.promo.models.PromoBanner -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.flowOf internal class DefaultPromoRepository( - private val tangemApi: TangemTechApi, private val appPreferencesStore: AppPreferencesStore, - private val dispatchers: CoroutineDispatcherProvider, ) : PromoRepository { - private val promoResponseConverter = PromoResponseConverter() - override suspend fun getChangellyPromoBanner(): PromoBanner? { - return runCatching(dispatchers.io) { - promoResponseConverter.convert( - tangemApi.getPromotionInfo(CHANGELLY_NAME) - .getOrThrow(), - ) - }.getOrNull() - } - - override suspend fun getOkxPromoBanner(): PromoBanner? { - return runCatching(dispatchers.io) { - promoResponseConverter.convert( - tangemApi.getPromotionInfo(OKX) - .getOrThrow(), - ) - }.getOrNull() - } - - override suspend fun getRingPromoBanner(): PromoBanner? { - return runCatching(dispatchers.io) { - promoResponseConverter.convert( - tangemApi.getPromotionInfo(RING) - .getOrThrow(), - ) - }.getOrNull() - } - override fun isReadyToShowWalletSwapPromo(): Flow { - return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, true) + return flowOf(false) // Use it on new promo action } override fun isReadyToShowTokenSwapPromo(): Flow { - return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, true) + return flowOf(false) // Use it on new promo action } override suspend fun setNeverToShowWalletSwapPromo() { - appPreferencesStore.store( - key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, - value = false, - ) + // Use it on new promo action } override suspend fun setNeverToShowTokenSwapPromo() { - appPreferencesStore.store( - key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, - value = false, - ) - } - - override fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow { - return combine( - flow = appPreferencesStore - .get(key = ADDED_WALLETS_WITH_RING_KEY, default = emptySet()) - .map { userWalletId.stringValue in it }, - flow2 = appPreferencesStore.get(key = SHOULD_SHOW_RING_PROMO_KEY, default = true), - transform = { isRingAdded, shouldShowRingPromo -> isRingAdded && shouldShowRingPromo }, - ) - } - - override suspend fun setNeverToShowRingPromo() { - appPreferencesStore.store(key = SHOULD_SHOW_RING_PROMO_KEY, value = false) + // Use it on new promo action } override fun isReadyToShowSwapStories(): Flow { @@ -105,10 +43,4 @@ internal class DefaultPromoRepository( value = false, ) } - - private companion object { - private const val CHANGELLY_NAME = "changelly" - private const val OKX = "okx" - private const val RING = "ring" - } } \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt index 102afaa289..40499944ec 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/di/PromoDataModule.kt @@ -1,10 +1,8 @@ package com.tangem.data.promo.di import com.tangem.data.promo.DefaultPromoRepository -import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.promo.PromoRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,11 +15,7 @@ internal object PromoDataModule { @Provides @Singleton - fun providePromoRepository( - tangemTechApi: TangemTechApi, - appPreferencesStore: AppPreferencesStore, - dispatchers: CoroutineDispatcherProvider, - ): PromoRepository { - return DefaultPromoRepository(tangemTechApi, appPreferencesStore, dispatchers) + fun providePromoRepository(appPreferencesStore: AppPreferencesStore): PromoRepository { + return DefaultPromoRepository(appPreferencesStore) } } \ No newline at end of file diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 6856b1236f..7997b79d5c 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -31,5 +33,7 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.timber) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion } diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index f79c7c6109..511b6f79b0 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -19,6 +21,7 @@ dependencies { implementation(projects.data.common) /** Domain modules */ + implementation(projects.core.configToggles) implementation(projects.domain.tokens.models) implementation(projects.domain.staking) implementation(projects.domain.wallets) @@ -45,6 +48,8 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.timber) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index f2f8cf8090..74c4a84423 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -44,6 +44,7 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade @@ -69,6 +70,7 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, private val getUserWalletUseCase: GetUserWalletUseCase, + private val stakingFeatureToggles: StakingFeatureToggles, moshi: Moshi, ) : StakingRepository { @@ -193,6 +195,8 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): StakingAvailability { + if (!checkFeatureToggleEnabled(cryptoCurrency.network.id)) return StakingAvailability.Unavailable + if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) return StakingAvailability.Unavailable val rawCurrencyId = cryptoCurrency.id.rawCurrencyId ?: return StakingAvailability.Unavailable @@ -216,6 +220,13 @@ internal class DefaultStakingRepository( } } + private fun checkFeatureToggleEnabled(networkId: Network.ID): Boolean { + return when (Blockchain.fromId(networkId.value)) { + Blockchain.TON -> stakingFeatureToggles.isTonStakingEnabled + else -> true + } + } + private fun checkForInvalidCardBatch(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { val userWallet = getUserWalletUseCase(userWalletId).getOrElse { error("Failed to get user wallet") @@ -245,7 +256,7 @@ internal class DefaultStakingRepository( params, ), ) - StakingActionCommonType.Exit -> stakeKitApi.createExitAction( + is StakingActionCommonType.Exit -> stakeKitApi.createExitAction( createActionRequestBody( userWalletId, network, @@ -275,7 +286,7 @@ internal class DefaultStakingRepository( params, ), ) - StakingActionCommonType.Exit -> stakeKitApi.estimateGasOnExit( + is StakingActionCommonType.Exit -> stakeKitApi.estimateGasOnExit( createActionRequestBody( userWalletId, network, @@ -562,8 +573,7 @@ internal class DefaultStakingRepository( } private fun getTransactionDataType(networkId: String, unsignedTransaction: String): TransactionData.Compiled.Data { - val blockchain = Blockchain.fromId(networkId) - return when (blockchain) { + return when (Blockchain.fromId(networkId)) { Blockchain.Solana, Blockchain.Cosmos, -> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes()) @@ -575,13 +585,14 @@ internal class DefaultStakingRepository( ?: error("Failed to parse Tron StakeKit transaction") TransactionData.Compiled.Data.RawString(tronStakeKitTransaction.rawDataHex) } + Blockchain.TON -> TransactionData.Compiled.Data.RawString(unsignedTransaction) else -> error("Unsupported blockchain") } } - private fun findPrefetchedYield(yields: List, currencyId: String, symbol: String): Yield? { + private fun findPrefetchedYield(yields: List, currencyId: CryptoCurrency.RawID, symbol: String): Yield? { return yields.find { yield -> - yield.tokens.any { it.coinGeckoId == currencyId && it.symbol == symbol } + yield.tokens.any { it.coinGeckoId == currencyId.value && it.symbol == symbol } } } @@ -621,6 +632,7 @@ internal class DefaultStakingRepository( private companion object { const val YIELDS_STORE_KEY = "yields" + const val TON_INTEGRATION_ID = "ton-ton-tonwhales-pools-staking" const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking" @@ -641,6 +653,7 @@ internal class DefaultStakingRepository( // uncomment items as implementation is ready val integrationIdMap = mapOf( + Blockchain.TON.run { id + toCoinId() } to TON_INTEGRATION_ID, Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID, Blockchain.Cosmos.run { id + toCoinId() } to COSMOS_INTEGRATION_ID, Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt index e3b284f5fc..82ebb3dbbb 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt @@ -75,6 +75,7 @@ class StakingNetworkTypeConverter : TwoWayConverter NetworkTypeDTO.SOLANA -> NetworkType.SOLANA NetworkTypeDTO.TEZOS -> NetworkType.TEZOS NetworkTypeDTO.TRON -> NetworkType.TRON + NetworkTypeDTO.TON -> NetworkType.TON else -> NetworkType.UNKNOWN } } @@ -147,6 +148,7 @@ class StakingNetworkTypeConverter : TwoWayConverter NetworkType.SOLANA -> NetworkTypeDTO.SOLANA NetworkType.TEZOS -> NetworkTypeDTO.TEZOS NetworkType.TRON -> NetworkTypeDTO.TRON + NetworkType.TON -> NetworkTypeDTO.TON else -> NetworkTypeDTO.UNKNOWN } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index 431bf32bf5..cc61ab8eae 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -95,9 +95,7 @@ class YieldConverter( rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming.asMandatory("rewardClaiming")), defaultValidator = metadataDTO.defaultValidator, minimumStake = metadataDTO.minimumStake, - supportsMultipleValidators = metadataDTO.supportsMultipleValidators.asMandatory( - "supportsMultipleValidators", - ), + supportsMultipleValidators = metadataDTO.supportsMultipleValidators, revshare = convertEnabled(metadataDTO.revshare.asMandatory("revshare")), fee = convertEnabled(metadataDTO.fee.asMandatory("fee")), ) @@ -146,6 +144,7 @@ class YieldConverter( ValidatorStatusDTO.DEACTIVATING -> ValidatorStatus.DEACTIVATING ValidatorStatusDTO.INACTIVE -> ValidatorStatus.INACTIVE ValidatorStatusDTO.JAILED -> ValidatorStatus.JAILED + ValidatorStatusDTO.FULL -> ValidatorStatus.FULL else -> ValidatorStatus.UNKNOWN } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 336bcb651e..3e777df680 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -2,12 +2,14 @@ package com.tangem.data.staking.di import com.squareup.moshi.Moshi import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.staking.* import com.tangem.data.staking.DefaultStakingErrorResolver import com.tangem.data.staking.DefaultStakingRepository import com.tangem.data.staking.DefaultStakingTransactionHashRepository import com.tangem.data.staking.converters.error.StakeKitErrorConverter +import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse import com.tangem.datasource.di.NetworkMoshi @@ -16,6 +18,7 @@ import com.tangem.datasource.local.token.StakingActionsStore import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.* +import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -39,6 +42,7 @@ internal object StakingDataModule { dispatchers: CoroutineDispatcherProvider, walletManagersFacade: WalletManagersFacade, getUserWalletUseCase: GetUserWalletUseCase, + stakingFeatureToggles: StakingFeatureToggles, @NetworkMoshi moshi: Moshi, ): StakingRepository { return DefaultStakingRepository( @@ -49,6 +53,7 @@ internal object StakingDataModule { dispatchers = dispatchers, walletManagersFacade = walletManagersFacade, getUserWalletUseCase = getUserWalletUseCase, + stakingFeatureToggles = stakingFeatureToggles, moshi = moshi, ) } @@ -91,4 +96,10 @@ internal object StakingDataModule { analyticsEventHandler = analyticsEventHandler, ) } + + @Provides + @Singleton + internal fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles { + return DefaultStakingFeatureToggles(featureTogglesManager) + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt new file mode 100644 index 0000000000..3b3259c021 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt @@ -0,0 +1,12 @@ +package com.tangem.data.staking.toggles + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.domain.staking.toggles.StakingFeatureToggles + +internal class DefaultStakingFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : StakingFeatureToggles { + + override val isTonStakingEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_TON_ENABLED") +} \ No newline at end of file diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index bf90a79e76..b268688860 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -48,4 +50,6 @@ dependencies { implementation(deps.timber) implementation(deps.retrofit) // For HttpException implementation(deps.androidx.paging.runtime) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 4250ccab0e..0a2312b1a8 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.repository.* import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -32,6 +33,7 @@ internal object TokensDataModule { cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, expressServiceLoader: ExpressServiceLoader, + hotCryptoLoader: HotCryptoLoader, excludedBlockchains: ExcludedBlockchains, ): CurrenciesRepository { return DefaultCurrenciesRepository( @@ -42,6 +44,7 @@ internal object TokensDataModule { appPreferencesStore = appPreferencesStore, dispatchers = dispatchers, expressServiceLoader = expressServiceLoader, + hotCryptoLoader = hotCryptoLoader, excludedBlockchains = excludedBlockchains, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt deleted file mode 100644 index 9cf7b0092c..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.data.tokens.paging - -import androidx.paging.PagingSource -import androidx.paging.PagingState -import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.model.Token -import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider - -/** - * A PagingSource responsible for retrieving tokens from the Tangem Tech API and adding to them quotes information. - * - * @property api Tangem Tech API - * @property quotesRepository Repository providing quotes data. - * @property dispatchers Coroutine dispatchers provider. - * @property searchText The search text used to filter tokens. - */ -internal class CoinsPagingSource( - private val api: TangemTechApi, - private val quotesRepository: QuotesRepository, - private val dispatchers: CoroutineDispatcherProvider, - private val searchText: String?, -) : PagingSource() { - - override fun getRefreshKey(state: PagingState): Int? { - return state.anchorPosition?.let { anchorPosition -> - state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1) - ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1) - } - } - - override suspend fun load(params: LoadParams): LoadResult { - val page = params.key ?: 0 - - return com.tangem.utils.coroutines.runCatching(dispatchers.io) { - api.getCoins( - active = true, // TODO change when voting functionality is implemented - searchText = searchText, - offset = page * params.loadSize, - limit = params.loadSize, - ).getOrThrow() - }.fold( - onSuccess = { response -> - val coinsIds = response.coins.map { coin -> - CryptoCurrency.ID( - prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, - body = CryptoCurrency.ID.Body.NetworkId(coin.id), - suffix = CryptoCurrency.ID.Suffix.RawID(coin.id), - ) - } - - try { - val quotes = quotesRepository.getQuotesSync(currenciesIds = coinsIds.toSet(), refresh = false) - LoadResult.Page( - data = CoinsResponseConverter.convert( - CoinsData( - response.coins, - response.imageHost, - quotes, - ), - ), - prevKey = if (page == 0) null else page.minus(other = 1), - nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1), - ) - } catch (t: Throwable) { - LoadResult.Error(t) - } - }, - onFailure = { LoadResult.Error(it) }, - ) - } -} - -data class CoinsData( - val coins: List, - val imageHost: String?, - val quotes: Set, -) \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt deleted file mode 100644 index c2b2108053..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.data.tokens.paging - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.data.common.currency.getNetworkStandardType -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Token -import com.tangem.utils.converter.Converter - -/** - * Converter from data model [CoinsResponse] to list of domain models [Token] - */ -internal object CoinsResponseConverter : Converter> { - - override fun convert(value: CoinsData): List { - return value.coins.map { coin -> - val id = CryptoCurrency.ID( - CryptoCurrency.ID.Prefix.TOKEN_PREFIX, - CryptoCurrency.ID.Body.NetworkId(coin.id), - CryptoCurrency.ID.Suffix.RawID(coin.id), - ) - val quote = value.quotes.find { it.rawCurrencyId == id.rawCurrencyId } - Token( - id = id.rawCurrencyId ?: coin.name, - name = coin.name, - symbol = coin.symbol, - iconUrl = getIconUrl(coin.id, value.imageHost), - isAvailable = coin.active, - networks = coin.networks.mapNotNull { network -> - val blockchain = Blockchain.fromNetworkId(network.networkId) ?: return@mapNotNull null - Token.Network( - networkId = network.networkId, - standardType = getNetworkStandardType(blockchain).name, - name = blockchain.fullName, - address = network.contractAddress, - iconUrl = getIconUrl(network.networkId, value.imageHost), - decimalCount = network.decimalCount?.toInt(), - ) - }, - quote = quote, - ) - } - } -} - -internal fun getIconUrl(id: String, imageHost: String? = null): String { - return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png" -} - -private const val DEFAULT_IMAGE_HOST = - "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/" \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 36fc695b7f..39f05b3b1c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -15,6 +15,7 @@ import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRA import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoLoader import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -33,10 +34,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.plus -import kotlinx.coroutines.withContext import timber.log.Timber import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency @@ -48,6 +47,7 @@ internal class DefaultCurrenciesRepository( private val cacheRegistry: CacheRegistry, private val appPreferencesStore: AppPreferencesStore, private val expressServiceLoader: ExpressServiceLoader, + private val hotCryptoLoader: HotCryptoLoader, private val dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ) : CurrenciesRepository { @@ -486,7 +486,9 @@ internal class DefaultCurrenciesRepository( } @OptIn(ExperimentalCoroutinesApi::class) - override fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> { + override fun getAllWalletsCryptoCurrencies( + currencyRawId: CryptoCurrency.RawID, + ): Flow>> { return userWalletsStore.userWallets.flatMapLatest { userWallets -> userWallets.forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) } @@ -496,7 +498,7 @@ internal class DefaultCurrenciesRepository( if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> val filterResponse = storedTokens.tokens.filter { - getL2CompatibilityTokenComparison(it, currencyRawId) + getL2CompatibilityTokenComparison(it, currencyRawId.value) } responseCurrenciesFactory.createCurrencies( @@ -601,7 +603,11 @@ internal class DefaultCurrenciesRepository( network = token.networkId, ) } - expressServiceLoader.update(userWalletId, tokens) + + coroutineScope { + launch { expressServiceLoader.update(userWalletId, tokens) } + launch { hotCryptoLoader.fetch(tokens = userTokens.tokens) } + } } private suspend fun fetchExpressAssetsByNetworkIds( @@ -619,7 +625,12 @@ internal class DefaultCurrenciesRepository( cacheRegistry.invokeOnExpire( key = getAssetsCacheKey(userWalletId), skipCache = refresh, - block = { expressServiceLoader.update(userWalletId, tokens) }, + block = { + coroutineScope { + launch { expressServiceLoader.update(userWalletId, tokens) } + launch { hotCryptoLoader.update(cryptoCurrencies) } + } + }, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index a680263464..0f38f7682d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -37,7 +37,7 @@ internal class DefaultQuotesRepository( private val mutex = Mutex() @OptIn(ExperimentalCoroutinesApi::class) - override fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean): Flow> { + override fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean): Flow> { return appPreferencesStore.getObject( key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, ) @@ -51,7 +51,7 @@ internal class DefaultQuotesRepository( .flowOn(dispatchers.io) } - override suspend fun fetchQuotes(currenciesIds: Set) { + override suspend fun fetchQuotes(currenciesIds: Set) { withContext(dispatchers.io) { val selectedAppCurrency = requireNotNull( value = appPreferencesStore.getObjectSyncOrNull( @@ -64,7 +64,7 @@ internal class DefaultQuotesRepository( } } - override suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set { + override suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set { return withContext(dispatchers.io) { val selectedAppCurrency = requireNotNull( value = appPreferencesStore.getObjectSyncOrNull( @@ -81,7 +81,7 @@ internal class DefaultQuotesRepository( } } - override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? { + override suspend fun getQuoteSync(currencyId: CryptoCurrency.RawID): Quote? { return withContext(dispatchers.io) { val setOfCurrencyId = setOf(currencyId) val quote = quotesStore.getSync(setOfCurrencyId).firstOrNull() @@ -90,7 +90,7 @@ internal class DefaultQuotesRepository( } private suspend fun fetchExpiredQuotes( - currenciesIds: Set, + currenciesIds: Set, appCurrencyId: String, refresh: Boolean, ) { @@ -110,15 +110,17 @@ internal class DefaultQuotesRepository( } } - private suspend fun fetchQuotes(rawCurrenciesIds: Set, appCurrencyId: String) { - val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies(rawCurrenciesIds) + private suspend fun fetchQuotes(rawCurrenciesIds: Set, appCurrencyId: String) { + val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( + rawCurrenciesIds.map { it.value }.toSet(), + ) val response = safeApiCallWithTimeout( call = { val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") tangemTechApi.getQuotes(appCurrencyId, coinIds).bind() }, onError = { error -> - cacheRegistry.invalidate(rawCurrenciesIds.map(::getQuoteCacheKey)) + cacheRegistry.invalidate(rawCurrenciesIds.map { getQuoteCacheKey(it) }) throw error }, @@ -132,21 +134,20 @@ internal class DefaultQuotesRepository( } private suspend fun filterExpiredCurrenciesIds( - currenciesIds: Set, + currenciesIds: Set, refresh: Boolean, - ): Set { + ): Set { return currenciesIds.fold(hashSetOf()) { acc, currencyId -> - val rawCurrencyId = currencyId.rawCurrencyId - if (rawCurrencyId != null && rawCurrencyId !in acc) { + if (currencyId !in acc) { cacheRegistry.invokeOnExpire( - key = getQuoteCacheKey(rawCurrencyId), + key = getQuoteCacheKey(currencyId), skipCache = refresh, - block = { acc.add(rawCurrencyId) }, + block = { acc.add(currencyId) }, ) } acc } } - private fun getQuoteCacheKey(rawCurrencyId: String): String = "quote_$rawCurrencyId" + private fun getQuoteCacheKey(rawCurrencyId: CryptoCurrency.RawID): String = "quote_${rawCurrencyId.value}" } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt index 79af4417b7..ec86873987 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt @@ -6,16 +6,16 @@ import com.tangem.domain.tokens.model.Quote import com.tangem.utils.converter.Converter import java.math.BigDecimal -typealias QuotesConverterValue = Pair, Set> +typealias QuotesConverterValue = Pair, Set> internal class QuotesConverter : Converter> { override fun convert(value: QuotesConverterValue): Set { val (setOfCurrencyId, setOfStoredQuote) = value return setOfCurrencyId.mapTo(hashSetOf()) { id -> - setOfStoredQuote.find { id.rawCurrencyId == it.rawCurrencyId } + setOfStoredQuote.find { id.value == it.rawCurrencyId } ?.let(::convertExistStoredQuote) - ?: Quote.Empty(id.rawCurrencyId) + ?: Quote.Empty(id) } } @@ -23,7 +23,7 @@ internal class QuotesConverter : Converter> { val (rawCurrencyId, responseQuote) = storedQuote return Quote.Value( - rawCurrencyId = rawCurrencyId, + rawCurrencyId = CryptoCurrency.RawID(rawCurrencyId), fiatRate = responseQuote.price ?: BigDecimal.ZERO, priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2), ) diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index e552eb58b0..5a74bcefe0 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -37,6 +39,8 @@ dependencies { implementation(deps.timber) implementation(deps.androidx.paging.runtime) implementation(deps.moshi.kotlin) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) /** Libs - Tangem */ implementation(deps.tangem.blockchain) diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index e558b2def0..f3211c1994 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -6,22 +6,25 @@ import androidx.paging.PagingData import arrow.fx.coroutines.parZip import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType -import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.visa.config.VisaLibLoader import com.tangem.data.visa.utils.* +import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.visa.model.VisaCurrency -import com.tangem.domain.visa.model.VisaTxDetails -import com.tangem.domain.visa.model.VisaTxHistoryItem +import com.tangem.domain.common.visa.VisaUtilities +import com.tangem.domain.visa.exception.RefreshTokenExpiredException +import com.tangem.domain.visa.model.* +import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.visa.repository.VisaRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.lib.visa.api.VisaApi import com.tangem.lib.visa.model.VisaTxHistoryResponse import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow @@ -29,13 +32,19 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext import java.math.BigDecimal +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.jvm.Throws -internal class DefaultVisaRepository( +@Singleton +internal class DefaultVisaRepository @Inject constructor( private val visaLibLoader: VisaLibLoader, private val tangemTechApi: TangemTechApi, private val cacheRegistry: CacheRegistry, private val userWalletsStore: UserWalletsStore, private val dispatchers: CoroutineDispatcherProvider, + private val visaAuthProvider: TangemVisaAuthProvider, + private val visaAuthRepository: VisaAuthRepository, ) : VisaRepository { private val currencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -106,11 +115,12 @@ internal class DefaultVisaRepository( cardPublicKey = cardPubKey, pageSize = pageSize, isRefresh = isRefresh, + userWallet = userWallet, ), cacheRegistry = cacheRegistry, - visaApi = api, fetchedItems = fetchedHistoryItems, dispatchers = dispatchers, + requestTxHistory = { offset, pageSize -> getTxHistory(api, userWalletId, offset, pageSize) }, ) }, ) @@ -134,6 +144,25 @@ internal class DefaultVisaRepository( } } + private suspend fun getTxHistory( + api: VisaApi, + userWalletId: UserWalletId, + offset: Int, + pageSize: Int, + ): VisaTxHistoryResponse = withContext(dispatchers.io) { + val userWallet = findVisaUserWallet(userWalletId) + val cardPubKey = getCardPubKey(userWallet) + + request(userWalletId = userWalletId) { + api.getTxHistory( + authorizationHeader = visaAuthProvider.getAuthHeader(userWallet.cardId), + cardPublicKey = cardPubKey, + limit = pageSize, + offset = offset, + ).getOrThrow() + } + } + private suspend fun makeAddress(userWalletId: UserWalletId): String { if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoAddress() @@ -166,9 +195,9 @@ internal class DefaultVisaRepository( if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoPublicKey() val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull { - it.curve == EllipticCurve.Secp256k1 + it.curve == VisaUtilities.mandatoryCurve } - requireNotNull(cardWallet) { "Secp256k1 card wallet not found" } + requireNotNull(cardWallet) { "Visa card wallet not found" } return cardWallet.publicKey.toHexString() } @@ -187,4 +216,56 @@ internal class DefaultVisaRepository( private fun getVisaCurrencyKey(address: String): String { return "visa_currency_$address" } + + private suspend fun request( + userWalletId: UserWalletId, + requestBlock: suspend () -> T, + ): T { + return runCatching { + requestBlock() + }.getOrElse { responseError -> + if (responseError !is ApiResponseError.HttpException || + responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED + ) { + throw responseError + } + + val authTokens = getAuthTokens(userWalletId) + val newTokens = runCatching { + visaAuthRepository.refreshAccessTokens(authTokens.refreshToken) + }.getOrElse { + if (it is ApiResponseError.HttpException && + it.code == ApiResponseError.HttpException.Code.UNAUTHORIZED + ) { + userWalletsStore.update(userWalletId) { userWallet -> + userWallet.copy( + scanResponse = userWallet.scanResponse.copy( + visaCardActivationStatus = VisaCardActivationStatus.RefreshTokenExpired + ) + ) + } + } + throw RefreshTokenExpiredException() + } + + userWalletsStore.update(userWalletId) { userWallet -> + userWallet.copy( + scanResponse = userWallet.scanResponse.copy( + visaCardActivationStatus = VisaCardActivationStatus.Activated( + visaAuthTokens = newTokens + ) + ) + ) + } + + requestBlock() + } + } + + @Throws + private suspend fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens { + val userWallet = findVisaUserWallet(userWalletId) + val status = userWallet.scanResponse.visaCardActivationStatus ?: error("Visa card activation status not found") + return (status as? VisaCardActivationStatus.Activated)?.visaAuthTokens ?: error("Visa card is not activated") + } } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt index 8c5dba275a..187f4ca9e6 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt @@ -1,7 +1,9 @@ package com.tangem.data.visa.config import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) internal data class VisaConfig( @Json(name = "testnet") val testnet: Addresses, @@ -11,6 +13,7 @@ internal data class VisaConfig( val header: Header, ) { + @JsonClass(generateAdapter = true) data class Addresses( @Json(name = "paymentAccountRegistry") val paymentAccountRegistry: String, @@ -18,6 +21,7 @@ internal data class VisaConfig( val bridgeProcessor: String, ) + @JsonClass(generateAdapter = true) data class Header( @Json(name = "x-asn") val xAsn: String, diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt index 60d487838e..efb081bdc6 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt @@ -7,6 +7,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.visa.repository.VisaRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,22 +16,10 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object ImplementedVisaDataModule { +internal interface ImplementedVisaDataModule { - @Provides + @Binds @Singleton @ImplementedVisaRepository - fun provideVisaRepository( - visaLibLoader: VisaLibLoader, - tangemTechApi: TangemTechApi, - cacheRegistry: CacheRegistry, - userWalletsStore: UserWalletsStore, - dispatchers: CoroutineDispatcherProvider, - ): VisaRepository = DefaultVisaRepository( - visaLibLoader, - tangemTechApi, - cacheRegistry, - userWalletsStore, - dispatchers, - ) + fun provideVisaRepository(impl: DefaultVisaRepository): VisaRepository } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt index 4af82f3014..e233435e55 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt @@ -3,8 +3,11 @@ package com.tangem.data.visa.utils import androidx.paging.PagingSource import androidx.paging.PagingState import com.tangem.data.common.cache.CacheRegistry +import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.domain.visa.model.VisaTxHistoryItem +import com.tangem.domain.wallets.models.UserWallet import com.tangem.lib.visa.api.VisaApi import com.tangem.lib.visa.model.VisaTxHistoryResponse import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -16,9 +19,9 @@ import timber.log.Timber internal class VisaTxHistoryPagingSource( params: Params, private val cacheRegistry: CacheRegistry, - private val visaApi: VisaApi, private val fetchedItems: MutableStateFlow>>, private val dispatchers: CoroutineDispatcherProvider, + val requestTxHistory: suspend (offset: Int, pageSize: Int) -> VisaTxHistoryResponse, ) : PagingSource() { private val itemsFactory = VisaTxHistoryItemFactory() @@ -73,11 +76,7 @@ internal class VisaTxHistoryPagingSource( } private suspend fun fetchItems(offset: Int, pageSize: Int) = withContext(dispatchers.io) { - val response = visaApi.getTxHistory( - cardPublicKey = cardPublicKey, - limit = pageSize, - offset = offset, - ).getOrThrow() + val response = requestTxHistory(offset, pageSize) fetchedItems.update { it.toMutableMap().apply { @@ -97,6 +96,7 @@ internal class VisaTxHistoryPagingSource( } class Params( + val userWallet: UserWallet, val cardPublicKey: String, val pageSize: Int, val isRefresh: Boolean, diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index a5be656c53..4384fc3d0e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -1,28 +1,49 @@ package com.tangem.data.visa +import com.tangem.common.extensions.toHexString +import com.tangem.crypto.CryptoUtils import com.tangem.data.visa.converter.VisaActivationStatusConverter +import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider import com.tangem.datasource.api.visa.TangemVisaApi import com.tangem.domain.visa.model.ActivationOrder import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.withContext -import javax.inject.Inject -import javax.inject.Singleton -@Singleton -internal class DefaultVisaActivationRepository @Inject constructor( +@Suppress("UnusedPrivateMember") +internal class DefaultVisaActivationRepository @AssistedInject constructor( + @Assisted private val cardId: String, private val visaApi: TangemVisaApi, private val dispatcherProvider: CoroutineDispatcherProvider, private val visaActivationStatusConverter: VisaActivationStatusConverter, + private val visaAuthProvider: TangemVisaAuthProvider, ) : VisaActivationRepository { override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) { - visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus()) + // visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus(visaAuthProvider.getAuthHeader(cardId))) + VisaActivationRemoteState.CardWalletSignatureRequired // mock // TODO implement refreshing access token if it's expired } - override suspend fun getActivationOrderToSign(): ActivationOrder { - return ActivationOrder("TODO implement") + override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState = + withContext(dispatcherProvider.io) { + // visaActivationStatusConverter.convert( + // visaApi.getRemoteActivationStatusLongPoll(visaAuthProvider.getAuthHeader(cardId)), + // ) + + VisaActivationRemoteState.WaitingPinCode + } + + override suspend fun getActivationOrderToSign(): ActivationOrder = withContext(dispatcherProvider.io) { + ActivationOrder(CryptoUtils.generateRandomBytes(length = 32).toHexString()) + } + + @AssistedFactory + interface Factory : VisaActivationRepository.Factory { + override fun create(cardId: String): DefaultVisaActivationRepository } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index a0fb323d81..5174396612 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -1,5 +1,7 @@ package com.tangem.data.visa +import com.tangem.common.extensions.toHexString +import com.tangem.crypto.CryptoUtils import com.tangem.datasource.api.visa.TangemVisaAuthApi import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaAuthSession @@ -10,6 +12,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import javax.inject.Inject +@Suppress("UnusedPrivateMember") internal class DefaultVisaAuthRepository @Inject constructor( private val visaAuthApi: TangemVisaAuthApi, private val dispatchers: CoroutineDispatcherProvider, @@ -17,14 +20,19 @@ internal class DefaultVisaAuthRepository @Inject constructor( override suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card = withContext(dispatchers.io) { - val response = visaAuthApi.generateNonceByCard( - cardId = cardId, - cardPublicKey = cardPublicKey, - ) + // val response = visaAuthApi.generateNonceByCard( + // cardId = cardId, + // cardPublicKey = cardPublicKey, + // ) + // + // VisaAuthChallenge.Card( + // challenge = response.nonce, + // session = VisaAuthSession(response.sessionId), + // ) VisaAuthChallenge.Card( - challenge = response.nonce, - session = VisaAuthSession(response.sessionId), + challenge = CryptoUtils.generateRandomBytes(length = 16).toHexString(), + session = VisaAuthSession("session"), ) } @@ -32,39 +40,56 @@ internal class DefaultVisaAuthRepository @Inject constructor( cardId: String, walletPublicKey: String, ): VisaAuthChallenge.Wallet = withContext(dispatchers.io) { - val response = visaAuthApi.generateNonceByWalletAddress( - customerId = cardId, - customerWalletAddress = walletPublicKey, - ) - + // val response = visaAuthApi.generateNonceByWalletAddress( + // customerId = cardId, + // customerWalletAddress = walletPublicKey, + // ) + // + // VisaAuthChallenge.Wallet( + // challenge = response.nonce, + // session = VisaAuthSession(response.sessionId), + // ) VisaAuthChallenge.Wallet( - challenge = response.nonce, - session = VisaAuthSession(response.sessionId), + challenge = CryptoUtils.generateRandomBytes(length = 32).toHexString(), + session = VisaAuthSession("session"), ) } override suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens = withContext(dispatchers.io) { - val response = when (signedChallenge) { - is VisaAuthSignedChallenge.ByCardPublicKey -> { - visaAuthApi.getAccessToken( - sessionId = signedChallenge.challenge.session.sessionId, - signature = signedChallenge.signature, - salt = signedChallenge.salt, - ) - } - is VisaAuthSignedChallenge.ByWallet -> { - visaAuthApi.getAccessToken( - sessionId = signedChallenge.challenge.session.sessionId, - signature = signedChallenge.signature, - salt = null, - ) - } - } - + // val response = when (signedChallenge) { + // is VisaAuthSignedChallenge.ByCardPublicKey -> { + // visaAuthApi.getAccessToken( + // sessionId = signedChallenge.challenge.session.sessionId, + // signature = signedChallenge.signature, + // salt = signedChallenge.salt, + // ) + // } + // is VisaAuthSignedChallenge.ByWallet -> { + // visaAuthApi.getAccessToken( + // sessionId = signedChallenge.challenge.session.sessionId, + // signature = signedChallenge.signature, + // salt = null, + // ) + // } + // } + // + // VisaAuthTokens( + // accessToken = response.accessToken, + // refreshToken = response.refreshToken, + // ) VisaAuthTokens( - accessToken = response.accessToken, - refreshToken = response.refreshToken, + accessToken = "accessToken", + refreshToken = VisaAuthTokens.RefreshToken("refreshToken"), + ) + } + + override suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens = + withContext(dispatchers.io) { + // TODO + VisaAuthTokens( + accessToken = "accessToken", + refreshToken = VisaAuthTokens.RefreshToken("new refreshToken"), ) } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index 1ffd8cec35..f1582ade9e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -38,5 +38,7 @@ internal interface VisaDataBindsModule { @Binds @Singleton - fun bindVisaActivationRepository(repository: DefaultVisaActivationRepository): VisaActivationRepository + fun bindVisaActivationRepositoryFactory( + repository: DefaultVisaActivationRepository.Factory, + ): VisaActivationRepository.Factory } \ No newline at end of file diff --git a/domain/analytics/build.gradle.kts b/domain/analytics/build.gradle.kts index e5e871ea2f..f649160e1e 100644 --- a/domain/analytics/build.gradle.kts +++ b/domain/analytics/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.kapt) id("configuration") } @@ -9,4 +10,6 @@ dependencies { implementation(projects.core.utils) implementation(projects.domain.core) implementation(projects.domain.wallets.models) + implementation(deps.moshi.kotlin) + kapt(deps.moshi.kotlin.codegen) } \ No newline at end of file diff --git a/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/model/WalletBalanceState.kt b/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/model/WalletBalanceState.kt index e9cde413a2..fab29c07ac 100644 --- a/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/model/WalletBalanceState.kt +++ b/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/model/WalletBalanceState.kt @@ -1,5 +1,13 @@ package com.tangem.domain.analytics.model +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) enum class WalletBalanceState { - ToppedUp, Empty, Error, + @Json(name = "ToppedUp") ToppedUp, + + @Json(name = "Empty") Empty, + + @Json(name = "Error") Error, } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/SetAccessCodeRequestPolicyUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/SetAccessCodeRequestPolicyUseCase.kt deleted file mode 100644 index e10f624726..0000000000 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/SetAccessCodeRequestPolicyUseCase.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.domain.card - -import com.tangem.domain.card.repository.CardSdkConfigRepository - -/** - * Set access code request policy by 'isBiometricsRequestPolicy' - * - * @property cardSdkConfigRepository repository for managing of CardSDK config - * -[REDACTED_AUTHOR] - */ -class SetAccessCodeRequestPolicyUseCase(private val cardSdkConfigRepository: CardSdkConfigRepository) { - - operator fun invoke(isBiometricsRequestPolicy: Boolean) { - cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy) - } -} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt index ef277955c2..fd2f4e3b54 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt @@ -15,6 +15,9 @@ interface CardSdkConfigRepository { @Deprecated("Use CardSdkConfigRepository's methods instead of this property") val sdk: TangemSdk + /** Allow requesting access codes from biometrically protected storage */ + var isBiometricsRequestPolicy: Boolean + /** Set access code request policy by [isBiometricsRequestPolicy] */ fun setAccessCodeRequestPolicy(isBiometricsRequestPolicy: Boolean) diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt index ea7727681f..639de2993b 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import java.math.BigDecimal -internal object BigDecimalSerializer : KSerializer { +object BigDecimalSerializer : KSerializer { override val descriptor = PrimitiveSerialDescriptor(serialName = "BigDecimal", PrimitiveKind.STRING) diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index ab9922a279..f87e9728c8 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -1,6 +1,9 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) id("configuration") } @@ -38,6 +41,7 @@ dependencies { implementation(deps.moshi.kotlin) implementation(deps.reKotlin) implementation(deps.timber) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) /** Testing libraries */ testImplementation(deps.test.junit) diff --git a/domain/legacy/proguard-rules.pro b/domain/legacy/proguard-rules.pro deleted file mode 100644 index ff59496d81..0000000000 --- a/domain/legacy/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/BlockchainNetwork.kt b/domain/legacy/src/main/java/com/tangem/domain/common/BlockchainNetwork.kt index 2b487680f9..a1a8cd1f18 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/BlockchainNetwork.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/BlockchainNetwork.kt @@ -1,5 +1,6 @@ package com.tangem.domain.common +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token @@ -8,8 +9,11 @@ import com.tangem.common.extensions.calculateHashCode @JsonClass(generateAdapter = true) @Deprecated("Use Network model") data class BlockchainNetwork( + @Json(name = "blockchain") val blockchain: Blockchain, + @Json(name = "derivationPath") val derivationPath: String?, + @Json(name = "tokens") val tokens: List, ) { diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt b/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt index 570071b37a..e4ff4eea14 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt @@ -1,6 +1,7 @@ package com.tangem.domain.common import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.domain.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.scan.CardDTO interface DerivationStyleProvider { @@ -8,14 +9,13 @@ interface DerivationStyleProvider { } internal class TangemDerivationStyleProvider( - private val cardTypesResolver: CardTypesResolver, private val card: CardDTO, ) : DerivationStyleProvider { override fun getDerivationStyle(): DerivationStyle? { return when { !card.settings.isHDWalletAllowed -> null firstBatchesOfWallet1(card) -> DerivationStyle.V1 - cardTypesResolver.isWallet2() -> DerivationStyle.V3 + card.isWallet2 -> DerivationStyle.V3 else -> DerivationStyle.V2 } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index bcf606539a..46035bd37d 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -8,6 +8,7 @@ import com.tangem.common.card.WalletData import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.TapWorkarounds.isWallet2 import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType @@ -35,10 +36,7 @@ internal class TangemCardTypesResolver( return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } - override fun isWallet2(): Boolean { - return card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && - card.settings.isKeysImportAllowed - } + override fun isWallet2(): Boolean = card.isWallet2 override fun isVisaWallet(): Boolean = productType == ProductType.Visa diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt index b369611972..493d724f8b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -23,6 +23,9 @@ object TapWorkarounds { val CardDTO.isStart2Coin: Boolean get() = isStart2CoinIssuer(issuer.name) + val CardDTO.isWallet2: Boolean + get() = firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && settings.isKeysImportAllowed + val CardDTO.isVisa: Boolean get() = VisaUtilities.isVisaCard(this) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt index baeb91fbed..6da98d7aa9 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt @@ -194,6 +194,10 @@ data object Wallet2CardConfig : CardConfig { Blockchain.OdysseyChainTestnet -> EllipticCurve.Secp256k1 Blockchain.Bitrock -> EllipticCurve.Secp256k1 Blockchain.BitrockTestnet -> EllipticCurve.Secp256k1 + Blockchain.Sonic -> EllipticCurve.Secp256k1 + Blockchain.SonicTestnet -> EllipticCurve.Secp256k1 + Blockchain.ApeChain -> EllipticCurve.Secp256k1 + Blockchain.ApeChainTestnet -> EllipticCurve.Secp256k1 } } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt index 94727f02cb..3c24fb103f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt @@ -24,10 +24,10 @@ val ScanResponse.cardTypesResolver: CardTypesResolver ) val ScanResponse.derivationStyleProvider: DerivationStyleProvider - get() = TangemDerivationStyleProvider( - cardTypesResolver, - card, - ) + get() = card.derivationStyleProvider + +val CardDTO.derivationStyleProvider: DerivationStyleProvider + get() = TangemDerivationStyleProvider(this) val UserWallet.cardTypesResolver: CardTypesResolver get() = scanResponse.cardTypesResolver diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt index 32d4282b20..d62fffa929 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaUtilities.kt @@ -16,14 +16,17 @@ object VisaUtilities { val visaBlockchain = Blockchain.Polygon - val visaDefaultDerivationPath = visaBlockchain.derivationPath(DerivationStyle.V3) + val visaDefaultDerivationPath + get() = visaBlockchain.derivationPath(DerivationStyle.V3) + + fun visaDefaultDerivationPath(style: DerivationStyle) = visaBlockchain.derivationPath(style) fun isVisaCard(card: CardDTO): Boolean { return isVisaCard(card.firmwareVersion.doubleValue, card.batchId) } fun isVisaCard(firmwareVersion: Double, batchId: String): Boolean { - return FirmwareVersion.visaRange.contains(firmwareVersion) && + return firmwareVersion in FirmwareVersion.visaRange && batchId.startsWith(VISA_BATCH_START) } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt new file mode 100644 index 0000000000..60a095ca16 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt @@ -0,0 +1,63 @@ +package com.tangem.domain.common.visa + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.blockchain.common.address.Address +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.scan.CardDTO + +object VisaWalletPublicKeyUtility { + + fun validateExtendedPublicKey(targetAddress: String, extendedPublicKey: ExtendedPublicKey): Either = + either { + validatePublicKey( + targetAddress = targetAddress, + publicKey = extendedPublicKey.publicKey, + ).bind() + } + + fun findKeyWithoutDerivation(targetAddress: String, card: CardDTO): Either = either { + val wallet = findWalletOnVisaCurve(card).bind() + + validatePublicKey( + targetAddress = targetAddress, + publicKey = wallet.publicKey, + ).bind() + + wallet.publicKey + } + + private fun findWalletOnVisaCurve(card: CardDTO): Either = either { + card.wallets.firstOrNull { it.curve == VisaUtilities.mandatoryCurve } ?: raise(Error.MissingWalletOnTargetCurve) + } + + private fun validatePublicKey(targetAddress: String, publicKey: ByteArray): Either = either { + val address = generateAddressOnVisaCurve(publicKey).bind() + + if (address.value != targetAddress) { + raise(Error.AddressNotMatched) + } + } + + private fun generateAddressOnVisaCurve(walletPublicKey: ByteArray): Either = either { + val addresses = catch( + block = { + VisaUtilities.visaBlockchain.makeAddresses( + walletPublicKey = walletPublicKey, + pairPublicKey = null, + curve = VisaUtilities.mandatoryCurve, + ) + }, + catch = { raise(Error.FailedToCreateAddress) }, + ) + + addresses.firstOrNull() ?: raise(Error.FailedToCreateAddress) + } + + enum class Error(val message: String) { + AddressNotMatched("ValidationError: Address not matched"), + FailedToCreateAddress("ValidationError: Failed to create address"), + MissingWalletOnTargetCurve("ValidationError: Missing wallet on target curve"), + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 440e818d8f..9811e8d408 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -217,7 +217,7 @@ class DefaultWalletManagersFacade( symbol = currency.symbol, contractAddress = currency.contractAddress, decimals = currency.decimals, - id = currency.id.rawCurrencyId, + id = currency.id.rawCurrencyId?.value, ) TransactionHistoryRequest.FilterType.Contract(blockchainToken) } @@ -255,7 +255,7 @@ class DefaultWalletManagersFacade( symbol = currency.symbol, contractAddress = currency.contractAddress, decimals = currency.decimals, - id = currency.id.rawCurrencyId, + id = currency.id.rawCurrencyId?.value, ) TransactionHistoryRequest.FilterType.Contract(blockchainToken) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt index 6ae9f6ae43..800cde28ef 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt @@ -1,5 +1,6 @@ package com.tangem.domain.walletmanager.model +import com.tangem.domain.tokens.model.CryptoCurrency import java.math.BigDecimal sealed class CryptoCurrencyAmount { @@ -9,7 +10,7 @@ sealed class CryptoCurrencyAmount { data class Coin(override val value: BigDecimal) : CryptoCurrencyAmount() data class Token( - val tokenId: String?, + val tokenId: CryptoCurrency.RawID?, val tokenContractAddress: String, override val value: BigDecimal, ) : CryptoCurrencyAmount() diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt index 53d7f303c5..d782bfdea2 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt @@ -15,7 +15,7 @@ internal class CryptoCurrencyTypeConverter : Converter { override fun convert(value: CryptoCurrency.Token): SdkToken { return SdkToken( - id = value.id.rawCurrencyId, + id = value.id.rawCurrencyId?.value, name = value.name, symbol = value.symbol, contractAddress = value.contractAddress, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt index e3a15932b4..6e9b4c2c1f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -2,6 +2,7 @@ package com.tangem.domain.walletmanager.utils import com.tangem.blockchain.common.* import com.tangem.blockchainsdk.utils.amountToCreateAccount +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletmanager.model.Address import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction @@ -84,7 +85,11 @@ internal class UpdateWalletManagerResultFactory { val demoAmounts = hashSetOf(CryptoCurrencyAmount.Coin(amountValue)) return tokens.mapTo(demoAmounts) { token -> - CryptoCurrencyAmount.Token(token.id, token.contractAddress, amountValue) + CryptoCurrencyAmount.Token( + tokenId = token.id?.let { CryptoCurrency.RawID(it) }, + tokenContractAddress = token.contractAddress, + value = amountValue, + ) } } @@ -107,7 +112,7 @@ internal class UpdateWalletManagerResultFactory { private fun createCurrencyAmount(amount: Amount): CryptoCurrencyAmount? { return when (val type = amount.type) { is AmountType.Token -> CryptoCurrencyAmount.Token( - tokenId = type.token.id, + tokenId = type.token.id?.let { CryptoCurrency.RawID(it) }, tokenContractAddress = type.token.contractAddress, value = getCurrencyAmountValue(amount) ?: return null, ) diff --git a/domain/legacy/src/test/java/com/tangem/domain/common/configs/Wallet2CardConfigTest.kt b/domain/legacy/src/test/java/com/tangem/domain/common/configs/Wallet2CardConfigTest.kt index 5ab98a3e72..dc94df361a 100644 --- a/domain/legacy/src/test/java/com/tangem/domain/common/configs/Wallet2CardConfigTest.kt +++ b/domain/legacy/src/test/java/com/tangem/domain/common/configs/Wallet2CardConfigTest.kt @@ -151,6 +151,10 @@ class Wallet2CardConfigTest { Blockchain.OdysseyChainTestnet to EllipticCurve.Secp256k1, Blockchain.Bitrock to EllipticCurve.Secp256k1, Blockchain.BitrockTestnet to EllipticCurve.Secp256k1, + Blockchain.Sonic to EllipticCurve.Secp256k1, + Blockchain.SonicTestnet to EllipticCurve.Secp256k1, + Blockchain.ApeChain to EllipticCurve.Secp256k1, + Blockchain.ApeChainTestnet to EllipticCurve.Secp256k1, ) @Test diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index dd2f977457..817a68b712 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -111,7 +111,7 @@ class SaveManagedTokensUseCase( is ManagedCryptoCurrency.SourceNetwork.Default -> customTokensRepository.createToken( managedCryptoCurrency = token, sourceNetwork = sourceNetwork, - rawId = token.id.value, + rawId = CryptoCurrency.RawID(token.id.value), ) is ManagedCryptoCurrency.SourceNetwork.Main -> customTokensRepository.createCoin( userWalletId = userWalletId, diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt index 3b0d13aba6..86db4be54c 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt @@ -33,7 +33,7 @@ interface CustomTokensRepository { suspend fun createToken( managedCryptoCurrency: ManagedCryptoCurrency.Token, sourceNetwork: ManagedCryptoCurrency.SourceNetwork.Default, - rawId: String?, + rawId: CryptoCurrency.RawID?, ): CryptoCurrency.Token suspend fun createCustomToken( diff --git a/domain/markets/models/build.gradle.kts b/domain/markets/models/build.gradle.kts index 7558c5ca61..dafb0e3da4 100644 --- a/domain/markets/models/build.gradle.kts +++ b/domain/markets/models/build.gradle.kts @@ -5,6 +5,7 @@ plugins { } dependencies { + api(projects.domain.tokens.models) implementation(projects.domain.core) implementation(deps.kotlin.serialization) diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt index e7d0c1d6e9..1724d9ce45 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -1,9 +1,10 @@ package com.tangem.domain.markets +import com.tangem.domain.tokens.model.CryptoCurrency import java.math.BigDecimal data class TokenMarket( - val id: String, + val id: CryptoCurrency.RawID, val name: String, val symbol: String, val marketRating: Int?, diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt index e14b1f417b..77c288aa8b 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt @@ -1,11 +1,12 @@ package com.tangem.domain.markets import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.serialization.Serializable @Serializable data class TokenMarketParams( - val id: String, + val id: CryptoCurrency.RawID, val name: String, val symbol: String, val tokenQuotes: Quotes, diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt index 0a8d0d931d..91a67b1d12 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -16,8 +16,10 @@ class GetCurrencyQuotesUseCase( interval: PriceChangeInterval, refresh: Boolean, ): Flow> { + val rawId = currencyID.rawCurrencyId ?: return flowOf(None) + return quotesRepository.getQuotesUpdates( - currenciesIds = setOf(currencyID), + currenciesIds = setOf(rawId), refresh = refresh, ).map { it.filterIsInstance().firstOrNull().toOption() }.catch { emit(None) } } diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenExchangesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenExchangesUseCase.kt index a2862b339f..7139071dd9 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenExchangesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenExchangesUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.markets import arrow.core.Either import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.tokens.model.CryptoCurrency /** * Get token exchanges use case @@ -12,7 +13,7 @@ class GetTokenExchangesUseCase( private val marketsTokenRepository: MarketsTokenRepository, ) { - suspend operator fun invoke(tokenId: String): Either> { + suspend operator fun invoke(tokenId: CryptoCurrency.RawID): Either> { return Either.catch { marketsTokenRepository.getTokenExchanges(tokenId = tokenId) } } } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenFullQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenFullQuotesUseCase.kt index e8b8470add..ee0b3fbac4 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenFullQuotesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenFullQuotesUseCase.kt @@ -3,13 +3,14 @@ package com.tangem.domain.markets import arrow.core.Either import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.tokens.model.CryptoCurrency class GetTokenFullQuotesUseCase( private val marketsTokenRepository: MarketsTokenRepository, ) { suspend operator fun invoke( appCurrency: AppCurrency, - tokenId: String, + tokenId: CryptoCurrency.RawID, tokenSymbol: String, ): Either { return Either.catch { diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt index 2038181eef..a330e614ce 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketInfoUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.markets import arrow.core.Either import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.SupportedLanguages class GetTokenMarketInfoUseCase( @@ -11,7 +12,7 @@ class GetTokenMarketInfoUseCase( suspend operator fun invoke( appCurrency: AppCurrency, - tokenId: String, + tokenId: CryptoCurrency.RawID, tokenSymbol: String, ): Either { return Either.catch { diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt index c6d677f34b..a47186eb3d 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenPriceChartUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.markets import arrow.core.Either import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.tokens.model.CryptoCurrency class GetTokenPriceChartUseCase( private val marketsTokenRepository: MarketsTokenRepository, @@ -11,7 +12,7 @@ class GetTokenPriceChartUseCase( suspend operator fun invoke( appCurrency: AppCurrency, interval: PriceChangeInterval, - tokenId: String, + tokenId: CryptoCurrency.RawID, tokenSymbol: String, preview: Boolean, ): Either { diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index 1f728f1a0f..80a1a97480 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -15,25 +15,29 @@ interface MarketsTokenRepository { suspend fun getChart( fiatCurrencyCode: String, interval: PriceChangeInterval, - tokenId: String, + tokenId: CryptoCurrency.RawID, tokenSymbol: String, ): TokenChart suspend fun getChartPreview( fiatCurrencyCode: String, interval: PriceChangeInterval, - tokenId: String, + tokenId: CryptoCurrency.RawID, tokenSymbol: String, ): TokenChart suspend fun getTokenInfo( fiatCurrencyCode: String, - tokenId: String, + tokenId: CryptoCurrency.RawID, tokenSymbol: String, languageCode: String, ): TokenMarketInfo - suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String, tokenSymbol: String): TokenQuotes + suspend fun getTokenQuotes( + fiatCurrencyCode: String, + tokenId: CryptoCurrency.RawID, + tokenSymbol: String, + ): TokenQuotes suspend fun createCryptoCurrency( userWalletId: UserWalletId, @@ -46,5 +50,5 @@ interface MarketsTokenRepository { * * @param tokenId token id */ - suspend fun getTokenExchanges(tokenId: String): List + suspend fun getTokenExchanges(tokenId: CryptoCurrency.RawID): List } \ No newline at end of file diff --git a/domain/models/build.gradle.kts b/domain/models/build.gradle.kts index 717c52e49c..fec72be756 100644 --- a/domain/models/build.gradle.kts +++ b/domain/models/build.gradle.kts @@ -1,6 +1,9 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.kotlin.jvm) alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) id("configuration") } @@ -11,4 +14,5 @@ dependencies { implementation(deps.moshi.kotlin) implementation(deps.moshi.adapters) implementation(deps.kotlin.serialization) + kapt(deps.moshi.kotlin.codegen) } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt index 706e05a1e5..721fbd04da 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt @@ -1,5 +1,6 @@ package com.tangem.domain.models.scan +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.tangem.common.card.Card import com.tangem.common.card.CardWallet @@ -17,20 +18,35 @@ import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion * */ @JsonClass(generateAdapter = true) data class CardDTO( + @Json(name = "cardId") val cardId: String, + @Json(name = "batchId") val batchId: String, + @Json(name = "cardPublicKey") val cardPublicKey: ByteArray, + @Json(name = "firmwareVersion") val firmwareVersion: FirmwareVersion, + @Json(name = "manufacturer") val manufacturer: Manufacturer, + @Json(name = "issuer") val issuer: Issuer, + @Json(name = "settings") val settings: Settings, + @Json(name = "userSettings") val userSettings: UserSettings?, + @Json(name = "linkedTerminalStatus") val linkedTerminalStatus: LinkedTerminalStatus, + @Json(name = "isAccessCodeSet") val isAccessCodeSet: Boolean, + @Json(name = "isPasscodeSet") val isPasscodeSet: Boolean?, + @Json(name = "supportedCurves") val supportedCurves: List, + @Json(name = "wallets") val wallets: List, + @Json(name = "attestation") val attestation: Attestation, + @Json(name = "backupStatus") val backupStatus: BackupStatus?, ) { constructor(card: Card) : this( @@ -92,16 +108,27 @@ data class CardDTO( @JsonClass(generateAdapter = true) data class Settings( + @Json(name = "securityDelay") val securityDelay: Int, + @Json(name = "maxWalletsCount") val maxWalletsCount: Int, + @Json(name = "isSettingAccessCodeAllowed") val isSettingAccessCodeAllowed: Boolean, + @Json(name = "isSettingPasscodeAllowed") val isSettingPasscodeAllowed: Boolean, + @Json(name = "isResettingUserCodesAllowed") val isResettingUserCodesAllowed: Boolean, + @Json(name = "isLinkedTerminalEnabled") val isLinkedTerminalEnabled: Boolean, + @Json(name = "isBackupAllowed") val isBackupAllowed: Boolean, + @Json(name = "supportedEncryptionModes") val supportedEncryptionModes: List, + @Json(name = "isFilesAllowed") val isFilesAllowed: Boolean, + @Json(name = "isHDWalletAllowed") val isHDWalletAllowed: Boolean, + @Json(name = "isKeysImportAllowed") val isKeysImportAllowed: Boolean = false, ) { constructor(settings: Card.Settings) : this( @@ -121,6 +148,7 @@ data class CardDTO( @JsonClass(generateAdapter = true) data class UserSettings( + @Json(name = "isUserCodeRecoveryAllowed") val isUserCodeRecoveryAllowed: Boolean, ) { constructor(userSettings: com.tangem.common.card.UserSettings) : this( @@ -130,9 +158,13 @@ data class CardDTO( @JsonClass(generateAdapter = true) data class FirmwareVersion( + @Json(name = "major") val major: Int, + @Json(name = "minor") val minor: Int, + @Json(name = "patch") val patch: Int, + @Json(name = "type") val type: SdkFirmwareVersion.FirmwareType, ) : Comparable { val stringValue: String @@ -161,8 +193,11 @@ data class CardDTO( @JsonClass(generateAdapter = true) data class Manufacturer( + @Json(name = "name") val name: String, + @Json(name = "manufactureDate") val manufactureDate: Date, + @Json(name = "signature") val signature: ByteArray?, ) { constructor(manufacturer: Card.Manufacturer) : this( @@ -195,7 +230,9 @@ data class CardDTO( @JsonClass(generateAdapter = true) data class Issuer( + @Json(name = "name") val name: String, + @Json(name = "publicKey") val publicKey: ByteArray, ) { constructor(issuer: Card.Issuer) : this( @@ -220,16 +257,27 @@ data class CardDTO( @JsonClass(generateAdapter = true) data class Wallet( + @Json(name = "publicKey") val publicKey: ByteArray, + @Json(name = "chainCode") val chainCode: ByteArray?, + @Json(name = "curve") val curve: EllipticCurve, + @Json(name = "settings") val settings: CardWallet.Settings, + @Json(name = "totalSignedHashes") val totalSignedHashes: Int?, + @Json(name = "remainingSignatures") val remainingSignatures: Int?, + @Json(name = "index") val index: Int, + @Json(name = "hasBackup") val hasBackup: Boolean, + @Json(name = "derivedKeys") val derivedKeys: Map, + @Json(name = "extendedPublicKey") val extendedPublicKey: ExtendedPublicKey?, + @Json(name = "isImported") val isImported: Boolean = false, ) { constructor(wallet: CardWallet) : this( @@ -281,9 +329,15 @@ data class CardDTO( } } + @JsonClass(generateAdapter = false) enum class LinkedTerminalStatus { + @Json(name = "Current") Current, + + @Json(name = "Other") Other, + + @Json(name = "None") None, ; diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt index 98954e2c5c..44caa2dc29 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt @@ -28,6 +28,26 @@ data class ScanResponse( typealias KeyWalletPublicKey = ByteArrayKey +@JsonClass(generateAdapter = false) enum class ProductType { - Note, Twins, Wallet, Start2Coin, Wallet2, Ring, Visa, + @Json(name = "Note") + Note, + + @Json(name = "Twins") + Twins, + + @Json(name = "Wallet") + Wallet, + + @Json(name = "Start2Coin") + Start2Coin, + + @Json(name = "Wallet2") + Wallet2, + + @Json(name = "Ring") + Ring, + + @Json(name = "Visa") + Visa, } \ No newline at end of file diff --git a/domain/onramp/models/build.gradle.kts b/domain/onramp/models/build.gradle.kts index dc13a99957..b717a7ae41 100644 --- a/domain/onramp/models/build.gradle.kts +++ b/domain/onramp/models/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.kotlin.jvm) alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) id("configuration") } @@ -9,6 +10,8 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(deps.moshi.kotlin) + kapt(deps.moshi.kotlin.codegen) implementation(deps.kotlin.serialization) implementation(deps.jodatime) } \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt new file mode 100644 index 0000000000..ad722d847a --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.onramp.model + +import com.tangem.domain.tokens.model.CryptoCurrency +import java.math.BigDecimal + +/** + * Hot crypto currency + * + * @property cryptoCurrency crypto currency + * @property fiatRate fiat rate + * @property priceChange price change + * +[REDACTED_AUTHOR] + */ +data class HotCryptoCurrency( + val cryptoCurrency: CryptoCurrency, + val fiatRate: BigDecimal, + val priceChange: BigDecimal, +) \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt index 8b5bdbf34b..ffb0107b88 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt @@ -1,12 +1,20 @@ package com.tangem.domain.onramp.model +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import kotlinx.serialization.Serializable @Serializable +@JsonClass(generateAdapter = true) data class OnrampCurrency( + @Json(name = "name") val name: String, + @Json(name = "code") val code: String, + @Json(name = "image") val image: String?, + @Json(name = "precision") val precision: Int, + @Json(name = "unit") val unit: String, ) \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt index 64aa740a5e..e116c0c085 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt @@ -49,6 +49,22 @@ data class OnrampStatus( -> false } + val isAutoDisposable: Boolean + get() = when (this) { + Finished, + -> true + Expired, + Failed, + Paused, + Created, + WaitingForPayment, + PaymentProcessing, + Verifying, + Paid, + Sending, + -> false + } + val isHidden: Boolean get() = when (this) { Created, diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt index 61c4d5798e..da977b2004 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt @@ -1,5 +1,7 @@ package com.tangem.domain.onramp.model.cache +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import com.tangem.domain.core.serialization.SerializedBigDecimal import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.OnrampStatus @@ -24,21 +26,38 @@ import com.tangem.domain.wallets.models.UserWalletId * @property paymentMethod Transaction payment method * @property paymentMethod Residency country */ +@JsonClass(generateAdapter = true) data class OnrampTransaction( + @Json(name = "txId") val txId: String, + @Json(name = "userWalletId") val userWalletId: UserWalletId, + @Json(name = "fromAmount") val fromAmount: SerializedBigDecimal, + @Json(name = "fromCurrency") val fromCurrency: OnrampCurrency, + @Json(name = "toAmount") val toAmount: SerializedBigDecimal, + @Json(name = "toCurrencyId") val toCurrencyId: String, + @Json(name = "status") val status: OnrampStatus.Status, + @Json(name = "externalTxUrl") val externalTxUrl: String?, + @Json(name = "externalTxId") val externalTxId: String?, + @Json(name = "timestamp") val timestamp: Long, + @Json(name = "providerName") val providerName: String, // todo onramp fix after SwapProvider moved to own module + @Json(name = "providerImageUrl") val providerImageUrl: String, // todo onramp fix after SwapProvider moved to own module + @Json(name = "providerType") val providerType: String, // todo onramp fix after SwapProvider moved to own module + @Json(name = "redirectUrl") val redirectUrl: String, + @Json(name = "paymentMethod") val paymentMethod: String, + @Json(name = "residency") val residency: String, ) \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetHotCryptoUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetHotCryptoUseCase.kt new file mode 100644 index 0000000000..13062a4be6 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetHotCryptoUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.onramp + +import com.tangem.domain.onramp.model.HotCryptoCurrency +import com.tangem.domain.onramp.repositories.HotCryptoRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Get hot crypto currencies + * + * @property hotCryptoRepository hot crypto repository + * +[REDACTED_AUTHOR] + */ +class GetHotCryptoUseCase( + private val hotCryptoRepository: HotCryptoRepository, +) { + + operator fun invoke(userWalletId: UserWalletId): Flow> { + return hotCryptoRepository.getCurrencies(userWalletId) + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/HotCryptoRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/HotCryptoRepository.kt new file mode 100644 index 0000000000..1e14c4dd2c --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/HotCryptoRepository.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.onramp.repositories + +import com.tangem.domain.onramp.model.HotCryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Hot crypto repository + * +[REDACTED_AUTHOR] + */ +interface HotCryptoRepository { + + /** Get currencies by [userWalletId] */ + fun getCurrencies(userWalletId: UserWalletId): Flow> +} \ No newline at end of file diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt index de2c366987..cc66ada854 100644 --- a/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/PromoRepository.kt @@ -1,17 +1,9 @@ package com.tangem.domain.promo -import com.tangem.domain.promo.models.PromoBanner -import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow interface PromoRepository { - suspend fun getChangellyPromoBanner(): PromoBanner? - - suspend fun getOkxPromoBanner(): PromoBanner? - - suspend fun getRingPromoBanner(): PromoBanner? - fun isReadyToShowWalletSwapPromo(): Flow fun isReadyToShowTokenSwapPromo(): Flow @@ -20,10 +12,6 @@ interface PromoRepository { suspend fun setNeverToShowTokenSwapPromo() - fun isReadyToShowRingPromo(userWalletId: UserWalletId): Flow - - suspend fun setNeverToShowRingPromo() - fun isReadyToShowSwapStories(): Flow suspend fun isReadyToShowSwapStoriesSync(): Boolean diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowRingPromoUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowRingPromoUseCase.kt deleted file mode 100644 index 78ad8931ef..0000000000 --- a/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowRingPromoUseCase.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.domain.promo - -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow - -class ShouldShowRingPromoUseCase(private val promoRepository: PromoRepository) { - - operator fun invoke(userWalletId: UserWalletId): Flow { - return promoRepository.isReadyToShowRingPromo(userWalletId) - } - - suspend fun neverToShow() = promoRepository.setNeverToShowRingPromo() -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldSaveAccessCodesUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldSaveAccessCodesUseCase.kt deleted file mode 100644 index b68ede4106..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldSaveAccessCodesUseCase.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.domain.settings - -import com.tangem.domain.settings.repositories.SettingsRepository - -class ShouldSaveAccessCodesUseCase(private val settingsRepository: SettingsRepository) { - - suspend operator fun invoke(): Boolean = settingsRepository.shouldSaveAccessCodes() -} \ No newline at end of file diff --git a/domain/staking/models/build.gradle.kts b/domain/staking/models/build.gradle.kts index 7558c5ca61..68f2c9b6df 100644 --- a/domain/staking/models/build.gradle.kts +++ b/domain/staking/models/build.gradle.kts @@ -1,6 +1,9 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.kotlin.jvm) alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) id("configuration") } @@ -9,4 +12,7 @@ dependencies { implementation(deps.kotlin.serialization) implementation(deps.jodatime) + + implementation(deps.moshi) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt index e57067b382..811b2ba5d1 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt @@ -1,6 +1,12 @@ package com.tangem.domain.staking.model +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) data class UnsubmittedTransactionMetadata( + @Json(name = "transactionHash") val transactionHash: String, + @Json(name = "transactionId") val transactionId: String, ) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt index 1d429683bc..2733a30688 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt @@ -67,5 +67,6 @@ enum class NetworkType { SOLANA, TEZOS, TRON, + TON, UNKNOWN, } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt index 9f201fb81e..d3ba1253c2 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt @@ -18,11 +18,11 @@ data class Yield( val isAvailable: Boolean, ) { + val allValidatorsFull: Boolean = validators.all { it.status == Validator.ValidatorStatus.FULL } + val preferredValidators: List get() = validators.filter { it.preferred } - fun getCurrentToken(rawCurrencyId: String?) = tokens.firstOrNull { rawCurrencyId == it.coinGeckoId } ?: token - @Serializable data class Status( val enter: Boolean, @@ -75,6 +75,7 @@ data class Yield( DEACTIVATING, INACTIVE, JAILED, + FULL, UNKNOWN, } } @@ -95,7 +96,7 @@ data class Yield( val rewardClaiming: RewardClaiming, val defaultValidator: String?, val minimumStake: Int?, - val supportsMultipleValidators: Boolean, + val supportsMultipleValidators: Boolean?, val revshare: Enabled, val fee: Enabled, ) { diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt index 0e2d7abe32..120dfd0c18 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt @@ -3,7 +3,7 @@ package com.tangem.domain.staking.model.stakekit.action sealed class StakingActionCommonType { data object Enter : StakingActionCommonType() - data object Exit : StakingActionCommonType() + data class Exit(val partiallyUnstakeDisabled: Boolean) : StakingActionCommonType() sealed class Pending : StakingActionCommonType() { data object Restake : Pending() data object Rewards : Pending() diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt new file mode 100644 index 0000000000..47949204f9 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.staking.toggles + +interface StakingFeatureToggles { + val isTonStakingEnabled: Boolean +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt index a0468f3100..017bc65de0 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt @@ -93,7 +93,7 @@ sealed class CryptoCurrency { } /** Represents a raw cryptocurrency ID. If it is a custom token, the value will be `null`. */ - val rawCurrencyId: String? get() = (suffix as? Suffix.RawID)?.rawId + val rawCurrencyId: RawID? get() = (suffix as? Suffix.RawID)?.rawId?.let { RawID(it) } val contractAddress: String? get() = (suffix as? Suffix.RawID)?.contractAddress @@ -196,6 +196,16 @@ sealed class CryptoCurrency { } } + /** + * Represents a raw cryptocurrency ID. Used for backend calls. + * Use with caution, as it does not provide the same level of uniqueness as [ID]. + */ + @Serializable + @JvmInline + value class RawID(val value: String) { + override fun toString(): String = value + } + protected fun checkProperties() { require(name.isNotBlank()) { "Crypto currency name must not be blank" } require(symbol.isNotBlank()) { "Crypto currency symbol must not be blank" } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt index d54b9a2342..c330160b97 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt @@ -4,14 +4,14 @@ import java.math.BigDecimal sealed interface Quote { - val rawCurrencyId: String? + val rawCurrencyId: CryptoCurrency.RawID /** * Represents unknown financial information for a specific cryptocurrency. * - * @property rawCurrencyId The raw cryptocurrency ID. If it is a custom token, the value will be `null`. + * @property rawCurrencyId The raw cryptocurrency ID. */ - data class Empty(override val rawCurrencyId: String?) : Quote + data class Empty(override val rawCurrencyId: CryptoCurrency.RawID) : Quote /** * Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change. @@ -21,7 +21,7 @@ sealed interface Quote { * @property priceChange The price change for the cryptocurrency. */ data class Value( - override val rawCurrencyId: String, + override val rawCurrencyId: CryptoCurrency.RawID, val fiatRate: BigDecimal, val priceChange: BigDecimal, ) : Quote diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt index 70f2a8632b..7ed6c36cc2 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenSwapPromoAnalyticsEvent.kt @@ -36,8 +36,8 @@ sealed class TokenSwapPromoAnalyticsEvent( } } + // Use it on new promo action enum class ProgramName { - OKX, - Ring, + Empty, } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/staking/YieldExtentions.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/staking/YieldExtentions.kt new file mode 100644 index 0000000000..d013d4e03b --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/staking/YieldExtentions.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.tokens.model.staking + +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.tokens.model.CryptoCurrency + +fun Yield.getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?) = + tokens.firstOrNull { rawCurrencyId?.value == it.coinGeckoId } ?: token \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index 0fc9607d48..9bac51630e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -37,7 +37,7 @@ class FetchCardTokenListUseCase( } val fetchQuotes = async { fetchQuotes( - currenciesIds = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::id), + currenciesIds = currencies.mapNotNullTo(destination = hashSetOf()) { it.id.rawCurrencyId }, refresh = refresh, ) } @@ -79,7 +79,7 @@ class FetchCardTokenListUseCase( ) } - private suspend fun fetchQuotes(currenciesIds: Set, refresh: Boolean) { + private suspend fun fetchQuotes(currenciesIds: Set, refresh: Boolean) { catch( block = { quotesRepository.getQuotesSync(currenciesIds, refresh) }, catch = { /* Ignore error */ }, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index b5a73a5ac3..a6455893a6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -125,7 +125,7 @@ class FetchCurrencyStatusUseCase( private suspend fun Raise.fetchQuote(currencyId: CryptoCurrency.ID, refresh: Boolean) { catch( - block = { quotesRepository.getQuotesSync(setOf(currencyId), refresh) }, + block = { quotesRepository.getQuotesSync(setOfNotNull(currencyId.rawCurrencyId), refresh) }, ) { raise(CurrencyStatusError.DataError(it)) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index f1ef16bca8..9b8dea0263 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -105,7 +105,10 @@ class FetchTokenListUseCase( private suspend fun fetchQuotes(currenciesIds: Set, refresh: Boolean) { catch( - block = { quotesRepository.getQuotesSync(currenciesIds, refresh) }, + block = { + val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet() + quotesRepository.getQuotesSync(rawIds, refresh) + }, ) { /* Ignore error */ } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt index 8fc8bef040..0dfb01856e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt @@ -41,7 +41,7 @@ class GetAllWalletsCryptoCurrencyStatusesUseCase( */ @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke( - currencyRawId: String, + currencyRawId: CryptoCurrency.RawID, ): Flow>>> { return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId) .flatMapLatest { userWalletsWithCurrencies: Map> -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt index aeba9d2c14..70564f24ef 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt @@ -47,7 +47,7 @@ class RefreshMultiCurrencyWalletQuotesUseCase( private suspend fun fetchQuotes(currenciesIds: Set) { catch( block = { - quotesRepository.fetchQuotes(currenciesIds) + quotesRepository.fetchQuotes(currenciesIds.mapNotNullTo(hashSetOf(), CryptoCurrency.ID::rawCurrencyId)) }, catch = { /* Ignore error */ }, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 16052e2974..a978af55f4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -166,7 +166,7 @@ internal class CurrenciesStatusesLceOperations( } private fun getQuotes(tokensIds: NonEmptySet): Flow>> { - return quotesRepository.getQuotesUpdates(tokensIds) + return quotesRepository.getQuotesUpdates(tokensIds.mapNotNull { it.rawCurrencyId }.toSet()) .map, Either>> { it.right() } .retryWhen { cause, _ -> emit(TokenListError.DataError(cause).left()) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index f2ef6e1efb..1035011308 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -31,7 +31,8 @@ internal class CurrenciesStatusesOperations( currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull() ?: return emptyList().right() val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right() + val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet() + val quotes = quotesRepository.getQuotesSync(rawIds, false).right() val networkStatuses = networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() val yieldBalances = getYieldBalancesSync(nonEmptyCurrencies) @@ -60,7 +61,8 @@ internal class CurrenciesStatusesOperations( } else { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, cryptoCurrencyId) } - val quotes = quotesRepository.getQuoteSync(cryptoCurrencyId)?.right() ?: Error.EmptyQuotes.left() + val quote = cryptoCurrencyId.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) }?.right() + ?: Error.EmptyQuotes.left() val networkStatuses = networksRepository.getNetworkStatusesSync( userWalletId, @@ -71,7 +73,7 @@ internal class CurrenciesStatusesOperations( }.right() val yieldBalances = getYieldBalanceSync(currency) - return createCurrencyStatus(currency, quotes, networkStatuses, yieldBalances) + return createCurrencyStatus(currency, quote, networkStatuses, yieldBalances) }, catch = { raise(Error.DataError(it)) }, ) @@ -104,7 +106,10 @@ internal class CurrenciesStatusesOperations( catch = { raise(Error.DataError(it)) }, ) val quotes = catch( - block = { quotesRepository.getQuoteSync(currency.id)?.right() ?: Error.EmptyQuotes.left() }, + block = { + currency.id.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) } + ?.right() ?: Error.EmptyQuotes.left() + }, catch = { Error.DataError(it).left() }, ) val networkStatus = catch( @@ -176,8 +181,7 @@ internal class CurrenciesStatusesOperations( fun getCurrencyStatusFlow( currency: CryptoCurrency, includeQuotes: Boolean = true, - ): Flow,> { + ): Flow> { val (networks, currenciesIds) = getIds(nonEmptyListOf(currency)) val quoteFlow = if (includeQuotes) { @@ -344,7 +348,8 @@ internal class CurrenciesStatusesOperations( } private fun getQuotes(tokensIds: NonEmptySet): Flow>> { - return quotesRepository.getQuotesUpdates(tokensIds) + val rawIds = tokensIds.mapNotNull { it.rawCurrencyId }.toSet() + return quotesRepository.getQuotesUpdates(rawIds) .map, Either>> { quotes -> if (quotes.isEmpty()) Error.EmptyQuotes.left() else quotes.right() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 41d94b2cf2..2c73402bff 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -74,7 +74,7 @@ internal class CurrencyStatusOperations( val yieldBalanceData = yieldBalance as? YieldBalance.Data val isCurrentAddressStaking = yieldBalanceData?.address == status.address.defaultAddress.value val filteredTokenBalances = yieldBalanceData?.balance?.items?.filter { - it.token.coinGeckoId == currency.id.rawCurrencyId + it.token.coinGeckoId == currency.id.rawCurrencyId?.value } val currentYieldBalance = if (isCurrentAddressStaking && filteredTokenBalances?.isNotEmpty() == true) { yieldBalanceData.copy( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index c99a692b70..04fa86295f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -248,7 +248,7 @@ interface CurrenciesRepository { ): CryptoCurrency.Token /** Get crypto currencies by [currencyRawId] from all user wallets */ - fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> + fun getAllWalletsCryptoCurrencies(currencyRawId: CryptoCurrency.RawID): Flow>> fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 57a8429697..98e2f0193c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -17,7 +17,7 @@ interface QuotesRepository { * @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved. * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. */ - fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean = false): Flow> + fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean = false): Flow> /** * Retrieves quotes for a set of specified cryptocurrencies, identified by their unique IDs. @@ -28,9 +28,9 @@ interface QuotesRepository { * @param refresh A boolean flag indicating whether the data should be refreshed. * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. */ - suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set + suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set - suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? + suspend fun getQuoteSync(currencyId: CryptoCurrency.RawID): Quote? - suspend fun fetchQuotes(currenciesIds: Set) + suspend fun fetchQuotes(currenciesIds: Set) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index 002f80eae8..5dde109c87 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import java.math.BigDecimal @@ -67,11 +68,10 @@ internal object MockQuotes { priceChange = BigDecimal("-0.10"), ) - val quote11 = Quote.Empty(null) - val quote12 = Quote.Empty("null") + val quote11 = Quote.Empty(CryptoCurrency.RawID("null")) val quotes = nonEmptySetOf( quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10, - quote11, quote12, + quote11, ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index ce99852888..a94add56b4 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -153,7 +153,9 @@ internal class MockCurrenciesRepository( error("not implemented") } - override fun getAllWalletsCryptoCurrencies(currencyRawId: String): Flow>> { + override fun getAllWalletsCryptoCurrencies( + currencyRawId: CryptoCurrency.RawID, + ): Flow>> { return emptyFlow() } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index 393432cf16..86f2775a96 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -13,18 +13,18 @@ internal class MockQuotesRepository( private val quotes: Flow>>, ) : QuotesRepository { - override fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean): Flow> { + override fun getQuotesUpdates(currenciesIds: Set, refresh: Boolean): Flow> { return quotes.map { it.getOrElse { e -> throw e } } } - override suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set { + override suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set { return getQuotesUpdates(currenciesIds).first() } - override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote { + override suspend fun getQuoteSync(currencyId: CryptoCurrency.RawID): Quote { return quotes.map { it.getOrElse { e -> throw e } }.first() - .first { it.rawCurrencyId == currencyId.rawCurrencyId } + .first { it.rawCurrencyId == currencyId } } - override suspend fun fetchQuotes(currenciesIds: Set) {} + override suspend fun fetchQuotes(currenciesIds: Set) {} } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ErrorsMapper.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ErrorsMapper.kt new file mode 100644 index 0000000000..5aa646c485 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ErrorsMapper.kt @@ -0,0 +1,50 @@ +package com.tangem.domain.transaction.error + +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.extensions.Result +import com.tangem.common.core.TangemSdkError +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.transaction.R +import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE +import com.tangem.sdk.extensions.localizedDescriptionRes + +fun Result.Failure.mapToFeeError(): GetFeeError { + return when (this.error) { + is BlockchainSdkError.Tron.AccountActivationError -> { + GetFeeError.BlockchainErrors.TronActivationError + } + is BlockchainSdkError.Kaspa.ZeroUtxoError -> { + GetFeeError.BlockchainErrors.KaspaZeroUtxo + } + else -> GetFeeError.DataError(this.error) + } +} + +fun parseWrappedError(error: BlockchainSdkError.WrappedTangemError): SendTransactionError { + return if (error.code == USER_CANCELLED_ERROR_CODE) { + SendTransactionError.UserCancelledError + } else { + when (val tangemError = error.tangemError) { + is TangemSdkError -> { + val resource = tangemError.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + SendTransactionError.TangemSdkError(tangemError.code, resId, wrappedList(resArgs)) + } + is BlockchainSdkError.WrappedTangemError -> { + parseWrappedError(tangemError) // todo remove when sdk errors are revised + } + is BlockchainSdkError.WrappedThrowable -> { + val causeError = tangemError.cause + if (causeError is BlockchainSdkError) { + SendTransactionError.BlockchainSdkError(causeError.code, causeError.customMessage) + } else { + SendTransactionError.BlockchainSdkError(tangemError.code, tangemError.customMessage) + } + } + else -> { + SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) + } + } + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/FeeErrorsMapper.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/FeeErrorsMapper.kt deleted file mode 100644 index 2bdbb7314f..0000000000 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/FeeErrorsMapper.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.domain.transaction.error - -import com.tangem.blockchain.common.BlockchainSdkError -import com.tangem.blockchain.extensions.Result - -fun Result.Failure.mapToFeeError(): GetFeeError { - return when (this.error) { - is BlockchainSdkError.Tron.AccountActivationError -> { - GetFeeError.BlockchainErrors.TronActivationError - } - is BlockchainSdkError.Kaspa.ZeroUtxoError -> { - GetFeeError.BlockchainErrors.KaspaZeroUtxo - } - else -> GetFeeError.DataError(this.error) - } -} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/RetryIncompleteTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/RetryIncompleteTransactionUseCase.kt index 9a227b2c03..83467e5a33 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/RetryIncompleteTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/RetryIncompleteTransactionUseCase.kt @@ -9,6 +9,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.transaction.error.IncompleteTransactionError import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.transaction.error.parseWrappedError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 12f09f23d3..59f1d6382f 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -9,8 +9,6 @@ import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.common.transaction.TransactionsSendResult import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.network.ResultChecker -import com.tangem.common.core.TangemSdkError -import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.simple import com.tangem.domain.card.repository.CardSdkConfigRepository @@ -19,13 +17,11 @@ import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.DemoTransactionSender import com.tangem.domain.tokens.model.Network -import com.tangem.domain.transaction.R import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE +import com.tangem.domain.transaction.error.parseWrappedError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import com.tangem.sdk.extensions.localizedDescriptionRes class SendTransactionUseCase( private val demoConfig: DemoConfig, @@ -132,33 +128,4 @@ class SendTransactionUseCase( } } } -} - -fun parseWrappedError(error: BlockchainSdkError.WrappedTangemError): SendTransactionError { - return if (error.code == USER_CANCELLED_ERROR_CODE) { - SendTransactionError.UserCancelledError - } else { - when (val tangemError = error.tangemError) { - is TangemSdkError -> { - val resource = tangemError.localizedDescriptionRes() - val resId = resource.resId ?: R.string.common_unknown_error - val resArgs = resource.args.map { it.value } - SendTransactionError.TangemSdkError(tangemError.code, resId, wrappedList(resArgs)) - } - is BlockchainSdkError.WrappedTangemError -> { - parseWrappedError(tangemError) // todo remove when sdk errors are revised - } - is BlockchainSdkError.WrappedThrowable -> { - val causeError = tangemError.cause - if (causeError is BlockchainSdkError) { - SendTransactionError.BlockchainSdkError(causeError.code, causeError.customMessage) - } else { - SendTransactionError.BlockchainSdkError(tangemError.code, tangemError.customMessage) - } - } - else -> { - SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) - } - } - } } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt index 11685b3866..f115a703f5 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt @@ -4,5 +4,6 @@ enum class VisaActivationError(val message: String) { BlockedForActivation("Card is blocked for activation"), InvalidActivationState("Invalid activation state"), WrongCard("Wrong card tapped"), + MissingWallet("Missing wallet"), MissingRootOTP("Missing root OTP"), } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt index 0341e9ad8a..7131b2533f 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt @@ -8,5 +8,10 @@ import kotlinx.serialization.Serializable @JsonClass(generateAdapter = true) data class VisaAuthTokens( @Json(name = "accessToken") val accessToken: String, - @Json(name = "refreshToken") val refreshToken: String, -) \ No newline at end of file + @Json(name = "refreshToken") val refreshToken: RefreshToken, +) { + + @Serializable + @JvmInline + value class RefreshToken(val value: String) +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationResponse.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationResponse.kt deleted file mode 100644 index 79cc4c9a95..0000000000 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationResponse.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.visa.model - -data class VisaCardActivationResponse( - val signedActivationOrder: SignedActivationOrder, - val rootOTP: VisaRootOTP, -) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt index 7e1a10a1c8..dc9e44c563 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt @@ -27,6 +27,9 @@ sealed class VisaCardActivationStatus { @Serializable data object Blocked : VisaCardActivationStatus() + @Serializable + data object RefreshTokenExpired : VisaCardActivationStatus() + companion object { val jsonAdapter: PolymorphicJsonAdapterFactory get() = PolymorphicJsonAdapterFactory.of(VisaCardActivationStatus::class.java, "type") diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataForApprove.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataForApprove.kt new file mode 100644 index 0000000000..93ec3d29d8 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataForApprove.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.visa.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class VisaDataForApprove( + @SerialName("customerWalletCardId") val customerWalletCardId: String?, + @SerialName("targetAddress") val targetAddress: String, + @SerialName("approveHash") val approveHash: String, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/exception/RefreshTokenExpiredException.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/exception/RefreshTokenExpiredException.kt new file mode 100644 index 0000000000..a125157184 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/exception/RefreshTokenExpiredException.kt @@ -0,0 +1,3 @@ +package com.tangem.domain.visa.exception + +class RefreshTokenExpiredException : Exception("Visa: refresh token expired") \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt index 8127f921c6..4dd591568f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt @@ -7,5 +7,11 @@ interface VisaActivationRepository { suspend fun getActivationRemoteState(): VisaActivationRemoteState + suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState + suspend fun getActivationOrderToSign(): ActivationOrder + + interface Factory { + fun create(cardId: String): VisaActivationRepository + } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt index a280b47b2c..1182e20b73 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt @@ -11,4 +11,6 @@ interface VisaAuthRepository { suspend fun getCustomerWalletAuthChallenge(cardId: String, walletPublicKey: String): VisaAuthChallenge.Wallet suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens + + suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens } \ No newline at end of file diff --git a/domain/wallets/models/build.gradle.kts b/domain/wallets/models/build.gradle.kts index 1b3ce4dfaf..9c259ee6b6 100644 --- a/domain/wallets/models/build.gradle.kts +++ b/domain/wallets/models/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.kotlin.jvm) alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) id("configuration") } @@ -15,5 +16,8 @@ dependencies { // region Other libraries implementation(deps.kotlin.serialization) + implementation(deps.moshi.kotlin) + implementation(deps.timber) + kapt(deps.moshi.kotlin.codegen) // endregion } \ No newline at end of file diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt index 4904076698..95b44fff95 100644 --- a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt @@ -1,5 +1,7 @@ package com.tangem.domain.wallets.models +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -14,13 +16,21 @@ import com.tangem.domain.models.scan.ScanResponse * @property isMultiCurrency Indicates whether this user wallet can work with more than one currency * @property scanResponse [ScanResponse] of primary user's wallet card. */ +@JsonClass(generateAdapter = true) data class UserWallet( + @Json(name = "name") val name: String, + @Json(name = "walletId") val walletId: UserWalletId, + @Json(name = "artworkUrl") val artworkUrl: String, + @Json(name = "cardsInWallet") val cardsInWallet: Set, + @Json(name = "isMultiCurrency") val isMultiCurrency: Boolean, + @Json(name = "hasBackupError") val hasBackupError: Boolean, + @Json(name = "scanResponse") val scanResponse: ScanResponse, // TODO: Replace with [com.tangem.domain.models.scan.CardDTO] ) { diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt index 4ea8ab824c..49e01f0922 100644 --- a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt @@ -1,11 +1,17 @@ package com.tangem.domain.wallets.models +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import kotlinx.serialization.Serializable @Serializable -data class UserWalletId(val stringValue: String) { +@JsonClass(generateAdapter = true) +data class UserWalletId( + @Json(name = "stringValue") + val stringValue: String, +) { val value = stringValue.hexToBytes() diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index c7d9e6d9fd..9144b3fbb0 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { /** Other dependencies */ implementation(deps.arrow.core) + implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index a1b849f65f..1174e7d634 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -9,10 +9,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.ButtonColors import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity @@ -22,8 +19,9 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.web.WebView +import com.google.accompanist.web.WebViewNavigator +import com.google.accompanist.web.rememberWebViewNavigator import com.google.accompanist.web.rememberWebViewState -import com.google.accompanist.web.rememberWebViewStateWithHTMLData import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.components.PrimaryButton @@ -42,6 +40,8 @@ import com.tangem.features.disclaimer.impl.entity.DisclaimerUM import com.tangem.features.disclaimer.impl.entity.DummyDisclaimer import com.tangem.features.disclaimer.impl.local.localTermsOfServices import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.withDebounce import java.nio.charset.StandardCharsets @Composable @@ -94,35 +94,34 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { private fun DisclaimerContent(url: String) { val backgroundColor = TangemTheme.colors.background.primary - val webViewStateUrl = rememberWebViewState(url) - val webViewStateData = rememberWebViewStateWithHTMLData( - data = localTermsOfServices, - mimeType = "text/html", - encoding = StandardCharsets.UTF_8.name(), - ) + val loadingTimeoutJobHolder = remember { JobHolder() } + var isLoading by remember { mutableStateOf(true) } - val webViewState by remember { - derivedStateOf { - if (webViewStateUrl.errorsForCurrentRequest.isNotEmpty()) { - webViewStateData - } else { - webViewStateUrl - } - } + val webViewState = rememberWebViewState(url) + val webViewNavigator = rememberWebViewNavigator() + val webViewClient = remember { + DisclaimerWebViewClient( + onLoadingFinished = { + loadingTimeoutJobHolder.cancel() + isLoading = false + }, + ) } Box { WebView( state = webViewState, - captureBackPresses = false, - onCreated = WebView::applySafeSettings, modifier = Modifier .fillMaxSize() .background(TangemTheme.colors.background.primary), + captureBackPresses = false, + navigator = webViewNavigator, + onCreated = WebView::applySafeSettings, + client = webViewClient, ) AnimatedVisibility( - visible = webViewState.isLoading, + visible = isLoading, label = "Loading state change animation", enter = fadeIn(), exit = fadeOut(), @@ -144,6 +143,28 @@ private fun DisclaimerContent(url: String) { } } } + + LaunchedEffect(loadingTimeoutJobHolder) { + withDebounce(jobHolder = loadingTimeoutJobHolder, timeMillis = 30_000) { + webViewNavigator.stopLoading() + + webViewNavigator.loadLocalToS() + } + } + + LaunchedEffect(webViewState.errorsForCurrentRequest) { + if (webViewState.errorsForCurrentRequest.isNotEmpty()) { + webViewNavigator.loadLocalToS() + } + } +} + +private fun WebViewNavigator.loadLocalToS() { + loadHtml( + html = localTermsOfServices, + mimeType = "text/html", + encoding = StandardCharsets.UTF_8.name(), + ) } @OptIn(ExperimentalPermissionsApi::class) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt new file mode 100644 index 0000000000..c5fbbfb32b --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt @@ -0,0 +1,56 @@ +package com.tangem.features.disclaimer.impl.ui + +import android.graphics.Bitmap +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebView +import com.google.accompanist.web.AccompanistWebViewClient +import timber.log.Timber + +/** + * [AccompanistWebViewClient] for Disclaimer [WebView] + * + * @property onLoadingFinished lambda be invoked when client will get a result of loading (content or error) + * +[REDACTED_AUTHOR] + */ +internal class DisclaimerWebViewClient( + private val onLoadingFinished: () -> Unit, +) : AccompanistWebViewClient() { + + private val loadedUrls: MutableMap = mutableMapOf() + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + Timber.d("onPageStarted: $url") + + if (url != null) { + loadedUrls[url] = false + } + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + + Timber.d("onPageFinished: $url") + + if (url != null && loadedUrls.containsKey(url)) { + loadedUrls[url] = true + onLoadingFinished() + } + } + + override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { + super.onReceivedError(view, request, error) + + val url = request?.url?.toString() + + Timber.d("onReceivedError: $url") + + if (url != null && loadedUrls.containsKey(url)) { + loadedUrls[url] = true + onLoadingFinished() + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index 2628335d48..3ee3a5edce 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -13,6 +13,7 @@ import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent @@ -38,7 +39,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( // applying l2 compatibility private val updatedParams = params.copy( token = params.token.copy( - id = getTokenIdIfL2Network(params.token.id), + id = CryptoCurrency.RawID(getTokenIdIfL2Network(params.token.id.value)), ), ) private val analyticsParams = params.analyticsParams diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index c6c2a86b49..04d2edcb0c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -134,7 +134,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( modelScope.launch { sendFeedbackEmailUseCase( type = FeedbackEmailType.CurrencyDescriptionError( - currencyId = params.token.id, + currencyId = params.token.id.value, currencyName = params.token.name, ), ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt index 1c63ecd0b4..4486732b67 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt @@ -17,7 +17,7 @@ import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.progressbar.LinearProgressIndicator +import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemAnimations @@ -116,7 +116,7 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi color = TangemTheme.colors.text.tertiary, ) } - LinearProgressIndicator( + TangemLinearProgressIndicator( modifier = Modifier .height(TangemTheme.dimens.size6) .fillMaxWidth(), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt index ac016c1bbf..f4e8c0aad9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt @@ -9,6 +9,7 @@ import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -37,7 +38,7 @@ internal class PortfolioDataLoader @Inject constructor( /** Load data by [currencyRawId] */ @OptIn(ExperimentalCoroutinesApi::class) - fun load(currencyRawId: String): Flow { + fun load(currencyRawId: CryptoCurrency.RawID): Flow { return combine( flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId), flow2 = getSelectedAppCurrencyFlow(), @@ -62,7 +63,7 @@ internal class PortfolioDataLoader @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) private fun getAllWalletsCryptoCurrenciesData( - currencyRawId: String, + currencyRawId: CryptoCurrency.RawID, ): Flow>> { return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) .distinctUntilChanged() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt index 05b33043d7..8599ad97d5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt @@ -24,7 +24,7 @@ internal class SelectNetworkUMConverter( override fun convert(value: TokenMarketParams): SelectNetworkUM { return SelectNetworkUM( - tokenId = value.id, + tokenId = value.id.value, iconUrl = value.imageUrl, tokenName = value.name, tokenCurrencySymbol = value.symbol, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index f3dd2d3c1a..7e94d860a3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -9,6 +9,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager @@ -48,7 +49,7 @@ internal class MarketsListModel @Inject constructor( initialValue = AppCurrency.Default, ) - private val visibleItemIds = MutableStateFlow>(emptyList()) + private val visibleItemIds = MutableStateFlow>(emptyList()) private val marketsListUMStateManager = MarketsListUMStateManager( currentVisibleIds = Provider { visibleItemIds.value }, @@ -72,8 +73,8 @@ internal class MarketsListModel @Inject constructor( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, currentAppCurrency = Provider { currentAppCurrency.value }, - currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval }, // FIXME fix on backend - currentSortByType = Provider { SortByTypeUM.Rating }, // FIXME maybe fix on backend + currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval }, + currentSortByType = Provider { SortByTypeUM.Rating }, currentSearchText = Provider { marketsListUMStateManager.searchQuery }, modelScope = modelScope, dispatchers = dispatchers, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt index bc5dcaed36..e178484e78 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListBatchFlowManager.kt @@ -2,6 +2,7 @@ package com.tangem.features.markets.tokenlist.impl.model.statemanager import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.tokenlist.impl.model.converters.MarketsTokenItemConverter import com.tangem.features.markets.tokenlist.impl.model.utils.logAction import com.tangem.features.markets.tokenlist.impl.model.utils.logStatus @@ -302,7 +303,7 @@ internal class MarketsListBatchFlowManager( } } - fun getBatchKeysByItemIds(ids: List): Set { + fun getBatchKeysByItemIds(ids: List): Set { val currentData = batchFlow.state.value.data return currentData @@ -311,7 +312,7 @@ internal class MarketsListBatchFlowManager( .toSet() } - fun getTokenById(id: String): TokenMarket? { + fun getTokenById(id: CryptoCurrency.RawID): TokenMarket? { return batchFlow.state.value.data.map { it.data }.flatten().find { it.id == id } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt index 6c2b177d24..aa7f006d19 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/statemanager/MarketsListUMStateManager.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.state.* import com.tangem.utils.Provider @@ -18,9 +19,9 @@ import kotlinx.coroutines.flow.update @Stable internal class MarketsListUMStateManager( - private val currentVisibleIds: Provider>, + private val currentVisibleIds: Provider>, private val onLoadMoreUiItems: () -> Unit, - private val visibleItemsChanged: (itemsKeys: List) -> Unit, + private val visibleItemsChanged: (itemsKeys: List) -> Unit, private val onRetryButtonClicked: () -> Unit, private val onTokenClick: (MarketsListItemUM) -> Unit, ) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 752717b1bb..e1b6bfd37b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -36,6 +36,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn @@ -298,7 +299,7 @@ private fun Preview() { items = MarketChartListItemPreviewDataProvider().values .flatMap { item -> List(size = 10) { item } } .mapIndexed { index, item -> - item.copy(id = index.toString()) + item.copy(id = CryptoCurrency.RawID(index.toString())) } .toImmutableList(), showUnder100kTokensNotification = false, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index 77b086ff3b..703e2eae67 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM import kotlinx.coroutines.launch @@ -88,7 +89,7 @@ internal fun MarketsListLazyColumn( is ListUM.Content -> { items( items = state.items, - key = { it.id + TOKEN_LAZY_LIST_ID_SEPARATOR + it.marketCap.toString() }, + key = { it.id.value + TOKEN_LAZY_LIST_ID_SEPARATOR + it.marketCap.toString() }, ) { item -> MarketsListItem( model = item, @@ -193,6 +194,7 @@ private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) { derivedStateOf { listState.layoutInfo.visibleItemsInfo.mapNotNull { (it.key as? String)?.split(TOKEN_LAZY_LIST_ID_SEPARATOR)?.first() + ?.let { rawId -> CryptoCurrency.RawID(rawId) } } } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt index e0c4a52d54..97eecee6d1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -4,13 +4,14 @@ package com.tangem.features.markets.tokenlist.impl.ui.preview import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import kotlinx.collections.immutable.persistentListOf internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( collection = listOf( MarketsListItemUM( - id = "1", + id = CryptoCurrency.RawID("1"), name = "Bitcoin", currencySymbol = "BTC", iconUrl = "", @@ -25,7 +26,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet isUnder100kMarketCap = false, ), MarketsListItemUM( - id = "1", + id = CryptoCurrency.RawID("1"), name = "Bitcoin", currencySymbol = "BTC", iconUrl = null, @@ -38,7 +39,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet isUnder100kMarketCap = false, ), MarketsListItemUM( - id = "1", + id = CryptoCurrency.RawID("1"), name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", currencySymbol = "BTC", iconUrl = null, @@ -53,7 +54,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet isUnder100kMarketCap = false, ), MarketsListItemUM( - id = "1", + id = CryptoCurrency.RawID("1"), name = "Bitcoin", currencySymbol = "BTC", iconUrl = null, @@ -68,7 +69,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet isUnder100kMarketCap = false, ), MarketsListItemUM( - id = "1", + id = CryptoCurrency.RawID("1"), name = "Bitcoin", currencySymbol = "BTC", iconUrl = null, @@ -83,7 +84,7 @@ internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParamet isUnder100kMarketCap = false, ), MarketsListItemUM( - id = "1", + id = CryptoCurrency.RawID("1"), name = "Bitcoin", currencySymbol = "BTC", iconUrl = null, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt index 9bc808aee8..458a1e29a3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListItemUM.kt @@ -4,10 +4,11 @@ import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.domain.tokens.model.CryptoCurrency @Immutable data class MarketsListItemUM( - val id: String, + val id: CryptoCurrency.RawID, val name: String, val currencySymbol: String, val iconUrl: String?, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt index 10ed82175e..298f90f22f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/state/MarketsListUM.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.markets.impl.R import kotlinx.collections.immutable.ImmutableList @@ -44,7 +45,7 @@ sealed class ListUM { val showUnder100kTokensNotification: Boolean, val showUnder100kTokensNotificationWasHidden: Boolean, val loadMore: () -> Unit, - val visibleIdsChanged: (List) -> Unit, + val visibleIdsChanged: (List) -> Unit, val onShowTokensUnder100kClicked: () -> Unit, val triggerScrollReset: StateEvent, val onItemClick: (MarketsListItemUM) -> Unit, diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 6a64d3f440..3f1c7150fc 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation(projects.domain.legacy) implementation(projects.domain.settings) implementation(projects.domain.onboarding) + implementation(projects.domain.visa) /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index ef564c6194..281ec29e57 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -72,6 +72,7 @@ internal class OnboardingEntryModel @Inject constructor( ProductType.Visa -> OnboardingRoute.Visa( scanResponse = scanResponse, titleProvider = titleProvider, + onDone = ::onVisaOnboardingDone, ) else -> error("Unsupported") } @@ -89,6 +90,12 @@ internal class OnboardingEntryModel @Inject constructor( } } + private fun onVisaOnboardingDone() { + stackNavigation.navigate { + listOf(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) + } + } + private fun navigateToWalletScreen() { modelScope.launch(NonCancellable) { router.replaceAll(AppRoute.Wallet) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt index a7488de99a..6e614deefd 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt @@ -31,6 +31,7 @@ internal class OnboardingChildFactory @Inject constructor( params = OnboardingVisaComponent.Params( scanResponse = route.scanResponse, titleProvider = route.titleProvider, + onDone = route.onDone, ), ) is OnboardingRoute.ManageTokens -> onboardingManageTokensComponentFactory.create( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt index 090724dff0..69778da3b4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt @@ -21,6 +21,7 @@ sealed class OnboardingRoute : Route { data class Visa( val titleProvider: TitleProvider, val scanResponse: ScanResponse, + val onDone: () -> Unit, ) : OnboardingRoute() data class ManageTokens( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt index 6e7321f878..51bf2171ca 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt @@ -54,7 +54,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor private val model: OnboardingMultiWalletModel = getOrCreateModel(params) private val stackNavigation = StackNavigation() - private val artworksState = instanceKeeper.getOrCreateSimple { + private val artworksState = instanceKeeper.getOrCreateSimple(key = "artworksState") { MutableStateFlow( when (model.state.value.currentStep) { CreateWallet -> WalletArtworksState.Folded @@ -68,9 +68,11 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor ) } - private val innerNavigationStateFlow = MutableStateFlow( - MultiWalletInnerNavigationState(1, 5), - ) + private val innerNavigationStateFlow = instanceKeeper.getOrCreateSimple(key = "innerNavigationState") { + MutableStateFlow( + MultiWalletInnerNavigationState(1, 5), + ) + } private val childParams = MultiWalletChildParams( multiWalletState = model.state, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt index 38e68c7642..2e91718db8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt @@ -43,6 +43,7 @@ internal fun MultiWalletAccessCodeEnter( stringResourceSafe(R.string.onboarding_access_code_intro_title) }, style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, ) Text( text = if (reEnterAccessCodeState) { @@ -51,6 +52,7 @@ internal fun MultiWalletAccessCodeEnter( stringResourceSafe(R.string.onboarding_access_code_hint) }, style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, ) val focusRequester = remember { FocusRequester() } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt index 0b4c0fe713..3f0fe0f290 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt @@ -28,6 +28,7 @@ internal fun MultiWalletAccessCodeIntro(modifier: Modifier = Modifier) { Text( text = stringResourceSafe(R.string.onboarding_access_code_intro_title), style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, ) Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_access_code), @@ -72,11 +73,13 @@ private fun DescriptionItem(title: String, body: String, @DrawableRes iconRes: I Text( text = title, style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, ) SpacerH(3.dp) Text( text = body, style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 6116ad29e2..c87784f4ad 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -7,6 +7,7 @@ import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType @@ -17,6 +18,7 @@ import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent @@ -30,6 +32,7 @@ import com.tangem.sdk.api.BackupServiceHolder import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.StringsSigns import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -48,6 +51,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private val userWalletsListManager: UserWalletsListManager, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, + private val walletsRepository: WalletsRepository, ) : Model() { private val params = paramsContainer.require() @@ -250,6 +254,10 @@ internal class MultiWalletFinalizeModel @Inject constructor( it.copy(resultUserWallet = userWallet) } + if (userWallet.scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported) { + launch(NonCancellable) { walletsRepository.markWallet2WasCreated(userWallet.walletId) } + } + // user wallet is fully created and saved, remove scan response from preferences // to prevent showing finalize screen dialog on next app start onboardingRepository.clearUnfinishedFinalizeOnboarding() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/MultiWalletSeedPhraseComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/MultiWalletSeedPhraseComponent.kt index 2a9df6122b..6e6722ddb7 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/MultiWalletSeedPhraseComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/MultiWalletSeedPhraseComponent.kt @@ -1,19 +1,14 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase -import android.app.Activity -import android.content.Context -import android.content.ContextWrapper -import android.view.WindowManager import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams @@ -79,30 +74,11 @@ internal class MultiWalletSeedPhraseComponent( model.onBack() } - // disable screenshots - val activity = LocalContext.current.findActivity() - DisposableEffect(activity) { - activity.window.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE, - ) - onDispose { - activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - } + DisableScreenshotsDisposableEffect() MultiWalletSeedPhrase( modifier = modifier, state = state, ) } - - private fun Context.findActivity(): Activity { - var context = this - while (context is ContextWrapper) { - if (context is Activity) return context - context = context.baseContext - } - error("") - } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseStart.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseStart.kt index b889ab83ba..7040a575f8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseStart.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseStart.kt @@ -40,6 +40,7 @@ internal fun MultiWalletSeedPhraseStart(state: MultiWalletSeedPhraseUM.Start, mo modifier = Modifier.size(56.dp), imageVector = ImageVector.vectorResource(R.drawable.ic_onboarding_text_edit_56), contentDescription = null, + tint = TangemTheme.colors.text.tertiary, ) Box( modifier = Modifier @@ -119,6 +120,7 @@ private fun ReadMoreBlock(state: MultiWalletSeedPhraseUM.Start, modifier: Modifi Text( modifier = Modifier.align(Alignment.CenterVertically), style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, text = stringResourceSafe(id = R.string.onboarding_seed_button_read_more), ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt index a22c50812e..50136e9d10 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.stepper.impl.ui +import android.content.res.Configuration import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween @@ -15,7 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.progressbar.LinearProgressIndicator +import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemAnimations import com.tangem.core.ui.res.TangemTheme @@ -59,7 +60,7 @@ internal fun OnboardingStepper( modifier = modifier, ) - LinearProgressIndicator( + TangemLinearProgressIndicator( modifier = Modifier .alpha(progressAlpha) .padding(horizontal = 16.dp) @@ -67,15 +68,16 @@ internal fun OnboardingStepper( .fillMaxWidth(), progress = { animatedIndicatorFraction }, color = TangemTheme.colors.icon.primary1, - backgroundColor = TangemTheme.colors.background.tertiary, + backgroundColor = TangemTheme.colors.icon.primary1.copy(alpha = 0.4f), strokeCap = StrokeCap.Round, ) } } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview() { +private fun OnboardingStepper_Preview() { TangemThemePreview { Box( Modifier diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt index 19cf83fac8..6aa983e077 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/api/OnboardingVisaComponent.kt @@ -11,6 +11,7 @@ interface OnboardingVisaComponent : ComposableContentComponent, InnerNavigationH data class Params( val scanResponse: ScanResponse, val titleProvider: TitleProvider, + val onDone: () -> Unit, ) interface Factory : ComponentFactory diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt index 67982e4535..da668753a6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt @@ -1,18 +1,38 @@ package com.tangem.features.onboarding.v2.visa.impl import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.stack.* +import com.arkivanov.decompose.value.Value +import com.arkivanov.decompose.value.observe +import com.arkivanov.essenty.instancekeeper.getOrCreateSimple import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerNavigation import com.tangem.core.decompose.navigation.inner.InnerNavigationState +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent +import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent +import com.tangem.features.onboarding.v2.visa.impl.child.approve.OnboardingVisaApproveComponent +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent +import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent +import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.OnboardingVisaOtherWalletComponent +import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent +import com.tangem.features.onboarding.v2.visa.impl.child.welcome.OnboardingVisaWelcomeComponent +import com.tangem.features.onboarding.v2.visa.impl.model.OnboardingVisaModel +import com.tangem.features.onboarding.v2.visa.impl.route.ONBOARDING_VISA_STEPS_COUNT +import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute +import com.tangem.features.onboarding.v2.visa.impl.route.screenTitle import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @Suppress("UnusedPrivateMember") @@ -21,29 +41,150 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( @Assisted private val params: OnboardingVisaComponent.Params, ) : OnboardingVisaComponent, AppComponentContext by appComponentContext { - private val innerNavigationState = MutableStateFlow( - OnboardingVisaInnerNavigationState( - stackSize = 1, - stackMaxSize = 7, - ), + private val model: OnboardingVisaModel = getOrCreateModel(params) + + private val currentChildBackEventHandle = instanceKeeper.getOrCreateSimple(key = "currentChildBackEventHandle") { + MutableSharedFlow() + } + + private val childParams = ChildParams( + onBack = ::onChildBack, + parentBackEvent = currentChildBackEventHandle, ) - private val currentChildBackEventHandle = MutableSharedFlow(onBufferOverflow = BufferOverflow.DROP_LATEST) + private val childStack: Value> = + childStack( + key = "innerStack", + source = model.stackNavigation, + serializer = null, + initialConfiguration = model.initialRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + createChild( + configuration, + childByContext(factoryContext), + ) + }, + ) override val innerNavigation: InnerNavigation = object : InnerNavigation { - override val state: StateFlow = innerNavigationState + override val state: StateFlow = model.innerNavigationState override fun pop(onComplete: (Boolean) -> Unit) { - // TODO add stack handling - componentScope.launch { currentChildBackEventHandle.emit(Unit) } + if (childStack.value.active.configuration is OnboardingVisaRoute.AccessCode) { + componentScope.launch { currentChildBackEventHandle.emit(Unit) } + } else { + onChildBack() + } } } + init { + // sets title and stepper value + childStack.observe(lifecycle) { stack -> + val currentRoute = stack.active.configuration + params.titleProvider.changeTitle(currentRoute.screenTitle()) + model.updateStepForNewRoute(currentRoute) + } + } + + @Suppress("LongMethod") + private fun createChild( + route: OnboardingVisaRoute, + factoryContext: AppComponentContext, + ): ComposableContentComponent { + return when (route) { + is OnboardingVisaRoute.Welcome -> OnboardingVisaWelcomeComponent( + appComponentContext = factoryContext, + params = OnboardingVisaWelcomeComponent.Params( + isWelcomeBack = route.isWelcomeBack, + childParams = childParams, + onDone = { model.navigateFromWelcome(route) }, + ), + ) + OnboardingVisaRoute.AccessCode -> OnboardingVisaAccessCodeComponent( + appComponentContext = factoryContext, + params = OnboardingVisaAccessCodeComponent.Params( + childParams = childParams, + onDone = { model.navigateFromAccessCode(it) }, + ), + config = OnboardingVisaAccessCodeComponent.Config( + scanResponse = model.currentScanResponse.value, + ), + ) + is OnboardingVisaRoute.ChooseWallet -> OnboardingVisaChooseWalletComponent( + appComponentContext = factoryContext, + params = OnboardingVisaChooseWalletComponent.Params( + childParams = childParams, + onEvent = { event -> model.navigateFromChooseWallet(route, event) }, + ), + ) + OnboardingVisaRoute.InProgress -> OnboardingVisaInProgressComponent( + appComponentContext = factoryContext, + config = OnboardingVisaInProgressComponent.Config( + scanResponse = model.currentScanResponse.value, + ), + params = OnboardingVisaInProgressComponent.Params( + childParams = childParams, + onDone = { model.stackNavigation.push(OnboardingVisaRoute.PinCode) }, + ), + ) + is OnboardingVisaRoute.OtherWalletApproveOption -> OnboardingVisaOtherWalletComponent( + appComponentContext = factoryContext, + config = OnboardingVisaOtherWalletComponent.Config( + scanResponse = model.currentScanResponse.value, + visaDataForApprove = route.visaDataForApprove, + ), + params = OnboardingVisaOtherWalletComponent.Params( + childParams = childParams, + onDone = { model.stackNavigation.push(OnboardingVisaRoute.PinCode) }, + ), + ) + OnboardingVisaRoute.PinCode -> OnboardingVisaPinCodeComponent( + appComponentContext = factoryContext, + config = OnboardingVisaPinCodeComponent.Config( + scanResponse = model.currentScanResponse.value, + ), + params = OnboardingVisaPinCodeComponent.Params( + childParams = childParams, + onDone = { params.onDone() }, + ), + ) + is OnboardingVisaRoute.TangemWalletApproveOption -> OnboardingVisaApproveComponent( + appComponentContext = factoryContext, + config = OnboardingVisaApproveComponent.Config( + visaDataForApprove = route.visaDataForApprove, + ), + params = OnboardingVisaApproveComponent.Params( + childParams = childParams, + onDone = { model.stackNavigation.push(OnboardingVisaRoute.InProgress) }, + ), + ) + } + } + + private fun onChildBack() = model.onChildBack( + currentRoute = childStack.value.active.configuration, + lastRoute = childStack.value.backStack.size == 1, + ) + @Composable override fun Content(modifier: Modifier) { - TODO("Not yet implemented") + val stackState by childStack.subscribeAsState() + + Children( + stack = stackState, + animation = stackAnimation(slide()), + ) { + it.instance.Content(modifier) + } } + data class ChildParams( + val onBack: () -> Unit, + val parentBackEvent: SharedFlow, + ) + @AssistedFactory interface Factory : OnboardingVisaComponent.Factory { override fun create( @@ -53,7 +194,8 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( } } -data class OnboardingVisaInnerNavigationState( +internal data class OnboardingVisaInnerNavigationState( override val stackSize: Int, - override val stackMaxSize: Int?, -) : InnerNavigationState \ No newline at end of file +) : InnerNavigationState { + override val stackMaxSize: Int = ONBOARDING_VISA_STEPS_COUNT +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt index 5ffff0e28f..5a8b17a5c2 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/OnboardingVisaAccessCodeComponent.kt @@ -8,27 +8,33 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model.OnboardingVisaAccessCodeModel import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.OnboardingVisaAccessCode -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch internal class OnboardingVisaAccessCodeComponent( appComponentContext: AppComponentContext, + config: Config, private val params: Params, ) : ComposableContentComponent, AppComponentContext by appComponentContext { - private val model: OnboardingVisaAccessCodeModel = getOrCreateModel() + private val model: OnboardingVisaAccessCodeModel = getOrCreateModel(config) init { componentScope.launch { - model.onBack.collect { params.onBack() } + model.onBack.collect { params.childParams.onBack() } } componentScope.launch { - params.parentBackEvent.collect { + params.childParams.parentBackEvent.collect { model.onBack() } } + componentScope.launch { + model.onDone.collect { params.onDone(it) } + } } @Composable @@ -37,11 +43,23 @@ internal class OnboardingVisaAccessCodeComponent( BackHandler(onBack = model::onBack) + // DisableScreenshotsDisposableEffect() + OnboardingVisaAccessCode(state, modifier) } + data class Config( + val scanResponse: ScanResponse, + ) + data class Params( - val parentBackEvent: SharedFlow, - val onBack: () -> Unit, + val childParams: DefaultOnboardingVisaComponent.ChildParams, + val onDone: (DoneEvent) -> Unit, + ) + + data class DoneEvent( + val visaDataForApprove: VisaDataForApprove, + val walletFound: Boolean, + val newScanResponse: ScanResponse, ) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index 24d171ebb9..0778d05cbe 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -1,27 +1,53 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model import androidx.compose.runtime.Stable +import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.common.CompletionResult +import com.tangem.common.extensions.toHexString import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.common.visa.VisaUtilities +import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility +import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @Stable @ComponentScoped internal class OnboardingVisaAccessCodeModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + @Suppress("UnusedPrivateMember") private val tangemSdkManager: TangemSdkManager, + private val visaAuthRepository: VisaAuthRepository, + private val getWalletsUseCase: GetWalletsUseCase, ) : Model() { + private val params: OnboardingVisaAccessCodeComponent.Config = paramsContainer.require() + private val activationStatus = + params.scanResponse.visaCardActivationStatus as? VisaCardActivationStatus.NotStartedActivation + ?: error("Visa activation status is not set or incorrect for this step") + private val _uiState = MutableStateFlow(getInitialState()) val uiState = _uiState.asStateFlow() val onBack = MutableSharedFlow() + val onDone = MutableSharedFlow() fun onBack() { + if (uiState.value.buttonLoading) return + when (uiState.value.step) { OnboardingVisaAccessCodeUM.Step.Enter -> modelScope.launch { onBack.emit(Unit) } OnboardingVisaAccessCodeUM.Step.ReEnter -> @@ -31,66 +57,143 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( private fun getInitialState(): OnboardingVisaAccessCodeUM { return OnboardingVisaAccessCodeUM( - onAccessCodeFirstChange = { _uiState.update { it.copy(accessCodeFirst = it.accessCodeFirst) } }, - onAccessCodeSecondChange = { _uiState.update { it.copy(accessCodeSecond = it.accessCodeSecond) } }, + onAccessCodeFirstChange = ::onAccessCodeFirstChange, + onAccessCodeSecondChange = ::onAccessCodeSecondChange, onAccessCodeHideClick = { _uiState.update { it.copy(accessCodeHidden = !it.accessCodeHidden) } }, onContinue = { onContinue() }, ) } - private fun onContinue() { - if (checkAccessCode().not()) return + private fun onAccessCodeFirstChange(textFieldValue: TextFieldValue) { + _uiState.update { + it.copy( + accessCodeFirst = textFieldValue, + atLeastMinCharsError = false, + ) + } + } + private fun onAccessCodeSecondChange(textFieldValue: TextFieldValue) { + _uiState.update { + it.copy( + accessCodeSecond = textFieldValue, + codesNotMatchError = false, + ) + } + } + + private fun onContinue() { when (uiState.value.step) { OnboardingVisaAccessCodeUM.Step.Enter -> { + if (checkAccessCodeMinChars().not()) return _uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.ReEnter) } } OnboardingVisaAccessCodeUM.Step.ReEnter -> { + if (checkAccessCodesMatch().not()) return startActivationProcess(accessCode = uiState.value.accessCodeFirst.text) } } } - private fun checkAccessCode(): Boolean { + private fun checkAccessCodeMinChars(): Boolean { val first = uiState.value.accessCodeFirst.text - val second = uiState.value.accessCodeSecond.text - when { - first.length < ACCESS_CODE_MIN_LENGTH -> { - _uiState.update { it.copy(atLeastMinCharsError = true) } - return false - } - first != second -> { - _uiState.update { it.copy(codesNotMatchError = true) } - return false - } + if (first.length < ACCESS_CODE_MIN_LENGTH) { + _uiState.update { it.copy(atLeastMinCharsError = true) } + return false } return true } + private fun checkAccessCodesMatch(): Boolean { + val first = uiState.value.accessCodeFirst.text + val second = uiState.value.accessCodeSecond.text + + if (first != second) { + _uiState.update { it.copy(codesNotMatchError = true) } + return false + } + + return true + } + + @Suppress("UnusedPrivateMember") private fun startActivationProcess(accessCode: String) { + loading(true) + modelScope.launch { - @Suppress("UnusedPrivateMember") + val challengeToSign = runCatching { + visaAuthRepository.getCardAuthChallenge( + cardId = activationStatus.activationInput.cardId, + cardPublicKey = activationStatus.activationInput.cardPublicKey.toHexString(), + ) + }.getOrElse { + loading(false) + // show alert + return@launch + } + val result = tangemSdkManager.activateVisaCard( accessCode = accessCode, - challengeToSign = null, - activationInput = TODO(), + challengeToSign = challengeToSign, + activationInput = activationStatus.activationInput, ) + // + when (result) { + is CompletionResult.Success -> { + // TODO load approve data from backend + val targetAddress = "x9F65354e595284956599F2892fA4A4a87653D6E6" + val foundCardId = tryToFindExistingWalletCardId(targetAddress) - // when (result) { - // is CompletionResult.Success -> { - // val response = result.data - // TODO() - // } - // is CompletionResult.Failure -> { - // // show alert - // TODO() - // } - // } + modelScope.launch { + onDone.emit( + OnboardingVisaAccessCodeComponent.DoneEvent( + visaDataForApprove = VisaDataForApprove( + targetAddress = targetAddress, + approveHash = "48b55c482123a10ad9022f9f4c5dd95c", + customerWalletCardId = foundCardId, + ), + walletFound = foundCardId != null, + newScanResponse = params.scanResponse.copy( + card = result.data.newCardDTO, + ), + ), + ) + } + } + is CompletionResult.Failure -> { + loading(false) + // show alert + } + } } } + private fun tryToFindExistingWalletCardId(targetAddress: String): String? { + val wallets = getWalletsUseCase.invokeSync().filter { it.isLocked.not() } + + return wallets.firstOrNull { wallet -> + wallet.scanResponse.card.wallets.any { + val derivedKey = it.derivedKeys[VisaUtilities.visaDefaultDerivationPath] ?: return@any false + + VisaWalletPublicKeyUtility.validateExtendedPublicKey( + targetAddress = targetAddress, + extendedPublicKey = derivedKey, + ).onLeft { + return@any VisaWalletPublicKeyUtility.findKeyWithoutDerivation( + targetAddress = targetAddress, + card = wallet.scanResponse.card, + ).isRight() + }.isRight() + } + }?.cardId + } + + private fun loading(state: Boolean) { + _uiState.update { it.copy(buttonLoading = state) } + } + private companion object { const val ACCESS_CODE_MIN_LENGTH = 4 } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt index f5c8364420..1557683bfe 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt @@ -43,7 +43,7 @@ internal fun OnboardingVisaAccessCode(state: OnboardingVisaAccessCodeUM, modifie .weight(1f), targetState = state.step, transitionSpec = TangemAnimations.AnimatedContent - .slide { initial, target -> initial.ordinal < target.ordinal }, + .slide { initial, target -> initial.ordinal > target.ordinal }, ) { step -> Content( state = state, @@ -65,6 +65,7 @@ internal fun OnboardingVisaAccessCode(state: OnboardingVisaAccessCodeUM, modifie }, onClick = state.onContinue, iconRes = R.drawable.ic_tangem_24, + showProgress = state.buttonLoading, isIconVisible = state.step == OnboardingVisaAccessCodeUM.Step.ReEnter, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt index 0cf7770ac0..35403457fd 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/state/OnboardingVisaAccessCodeUM.kt @@ -2,13 +2,14 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state import androidx.compose.ui.text.input.TextFieldValue -data class OnboardingVisaAccessCodeUM( +internal data class OnboardingVisaAccessCodeUM( val step: Step = Step.Enter, val accessCodeFirst: TextFieldValue = TextFieldValue(""), val accessCodeSecond: TextFieldValue = TextFieldValue(""), val codesNotMatchError: Boolean = false, val atLeastMinCharsError: Boolean = false, val accessCodeHidden: Boolean = true, + val buttonLoading: Boolean = false, val onAccessCodeFirstChange: (TextFieldValue) -> Unit = {}, val onAccessCodeSecondChange: (TextFieldValue) -> Unit = {}, val onContinue: () -> Unit = {}, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt new file mode 100644 index 0000000000..60141475ad --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/OnboardingVisaApproveComponent.kt @@ -0,0 +1,52 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.approve + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent +import com.tangem.features.onboarding.v2.visa.impl.child.approve.model.OnboardingVisaApproveModel +import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.OnboardingVisaApprove +import kotlinx.coroutines.launch + +internal class OnboardingVisaApproveComponent( + appComponentContext: AppComponentContext, + config: Config, + private val params: Params, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: OnboardingVisaApproveModel = getOrCreateModel(config) + + init { + componentScope.launch { + model.onDone.collect { params.onDone() } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) + + OnboardingVisaApprove( + state = state, + modifier = modifier, + ) + } + + data class Config( + val visaDataForApprove: VisaDataForApprove, + ) + + data class Params( + val childParams: DefaultOnboardingVisaComponent.ChildParams, + val onDone: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt new file mode 100644 index 0000000000..a79fe4aa78 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt @@ -0,0 +1,65 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.approve.model + +import androidx.compose.runtime.Stable +import com.tangem.common.CompletionResult +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.onboarding.v2.visa.impl.child.approve.OnboardingVisaApproveComponent +import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state.OnboardingVisaApproveUM +import com.tangem.sdk.api.TangemSdkManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ComponentScoped +internal class OnboardingVisaApproveModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val tangemSdkManager: TangemSdkManager, +) : Model() { + + private val params = paramsContainer.require() + private val _uiState = MutableStateFlow(getInitialState()) + + val uiState = _uiState.asStateFlow() + val onDone = MutableSharedFlow() + + private fun getInitialState(): OnboardingVisaApproveUM { + return OnboardingVisaApproveUM( + onApproveClick = ::onApproveClick, + ) + } + + private fun onApproveClick() { + loading(true) + + modelScope.launch { + val result = tangemSdkManager.visaCustomerWalletApprove( + visaDataForApprove = params.visaDataForApprove, + ) + + when (result) { + is CompletionResult.Success -> { + // TODO make backend call + + onDone.emit(Unit) + } + is CompletionResult.Failure -> { + loading(false) + // TODO show dialog + return@launch + } + } + } + } + + private fun loading(state: Boolean) { + _uiState.update { it.copy(approveButtonLoading = state) } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt new file mode 100644 index 0000000000..f93ebd7cd3 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt @@ -0,0 +1,84 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.approve.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state.OnboardingVisaApproveUM + +@Composable +internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Modifier = Modifier) { + Column( + modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(top = 56.dp, bottom = 32.dp) + .padding(horizontal = 34.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_card_grey), + contentDescription = null, + modifier = Modifier.fillMaxWidth(), + ) + + SpacerH(86.dp) + + Text( + text = "Prepare Tangem Wallet", + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + SpacerH(12.dp) + + Text( + text = "Prepare the Tangem card\nand tap to approve", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } + + NavigationPrimaryButton( + modifier = Modifier + .imePadding() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + primaryButton = NavigationButton( + textReference = TextReference.Str("Approve"), + iconRes = R.drawable.ic_tangem_24, + isIconVisible = true, + showProgress = state.approveButtonLoading, + onClick = state.onApproveClick, + ), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + OnboardingVisaApprove(state = OnboardingVisaApproveUM()) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/state/OnboardingVisaApproveUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/state/OnboardingVisaApproveUM.kt new file mode 100644 index 0000000000..60bf3ff09d --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/state/OnboardingVisaApproveUM.kt @@ -0,0 +1,6 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state + +internal data class OnboardingVisaApproveUM( + val approveButtonLoading: Boolean = false, + val onApproveClick: () -> Unit = {}, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/OnboardingVisaChooseWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/OnboardingVisaChooseWalletComponent.kt new file mode 100644 index 0000000000..3c8967551c --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/OnboardingVisaChooseWalletComponent.kt @@ -0,0 +1,51 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model.OnboardingVisaChooseWalletModel +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.OnboardingVisaChooseWallet +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +internal class OnboardingVisaChooseWalletComponent( + appComponentContext: AppComponentContext, + private val params: Params, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: OnboardingVisaChooseWalletModel = getOrCreateModel() + + init { + model.onEvent + .onEach { params.onEvent(it) } + .launchIn(componentScope) + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) + + OnboardingVisaChooseWallet( + modifier = modifier, + state = state, + ) + } + + data class Params( + val childParams: DefaultOnboardingVisaComponent.ChildParams, + val onEvent: (Event) -> Unit, + ) { + enum class Event { + TangemWallet, OtherWallet + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt new file mode 100644 index 0000000000..4f6629cae9 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt @@ -0,0 +1,59 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent.Params.Event +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.OnboardingVisaChooseWalletUM +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.SelectableChainRowUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ComponentScoped +internal class OnboardingVisaChooseWalletModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val _uiState = MutableStateFlow(getInitialState()) + + val onEvent = MutableSharedFlow() + val uiState = _uiState.asStateFlow() + + private fun getInitialState(): OnboardingVisaChooseWalletUM { + val selectedOption = SelectableChainRowUM( + event = Event.TangemWallet, + icon = R.drawable.ic_tangem_24, + text = TextReference.Str("Tangem Wallet"), + ) + + return OnboardingVisaChooseWalletUM( + options = persistentListOf( + selectedOption, + SelectableChainRowUM( + event = Event.OtherWallet, + icon = R.drawable.ic_wallet_filled_24, + text = TextReference.Str("Other Wallet"), + ), + ), + selectedOption = selectedOption, + onOptionSelected = { option -> _uiState.update { it.copy(selectedOption = option) } }, + onContinueClick = ::onContinueClicked, + ) + } + + private fun onContinueClicked() { + modelScope.launch { + val selectedOption = _uiState.value.selectedOption ?: return@launch + onEvent.emit(selectedOption.event) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt new file mode 100644 index 0000000000..b44e28c2c6 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt @@ -0,0 +1,174 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.components.rows.RowContentContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.outsetBorder +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.OnboardingVisaChooseWalletUM +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.SelectableChainRowUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun OnboardingVisaChooseWallet(state: OnboardingVisaChooseWalletUM, modifier: Modifier = Modifier) { + Column( + modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + Column( + Modifier + .weight(1f) + .padding(top = 12.dp) + .padding(horizontal = 16.dp), + ) { + Notification( + config = NotificationConfig( + subtitle = TextReference.Str( + "Please choose the wallet you started the registration" + + " process with to sign the transaction for creating your account on the Blockchain.", + ), + iconResId = R.drawable.ic_alert_circle_24, + ), + iconTint = TangemTheme.colors.icon.accent, + ) + + Text( + modifier = Modifier + .padding( + top = 12.dp, + bottom = 8.dp, + start = 12.dp, + end = 12.dp, + ), + text = "Choose wallet", + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + + SelectableChainRows( + items = state.options, + selected = state.selectedOption, + onClick = state.onOptionSelected, + ) + } + + PrimaryButton( + modifier = Modifier + .imePadding() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = "Continue", + onClick = state.onContinueClick, + ) + } +} + +@Composable +private fun SelectableChainRows( + items: ImmutableList, + selected: SelectableChainRowUM?, + onClick: (SelectableChainRowUM) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier) { + items.forEach { item -> + SelectableChainRow( + state = item, + selected = item == selected, + onClick = { onClick(item) }, + ) + } + } +} + +@Composable +private fun SelectableChainRow( + state: SelectableChainRowUM, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + RowContentContainer( + modifier = modifier + .heightIn(min = 48.dp) + .outsetBorder( + color = if (selected) TangemTheme.colors.icon.accent.copy(alpha = 0.15f) else Color.Transparent, + width = 5.dp, + shape = RoundedCornerShape(size = 18.dp), + ) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .border( + width = 1.dp, + color = if (selected) TangemTheme.colors.icon.accent else Color.Transparent, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .clickable(onClick = onClick) + .padding(12.dp), + icon = { + Icon( + imageVector = ImageVector.vectorResource(id = state.icon), + tint = if (selected) TangemTheme.colors.icon.primary1 else TangemTheme.colors.icon.informative, + contentDescription = null, + ) + }, + text = { + Text( + text = state.text.resolveReference(), + style = TangemTheme.typography.button, + color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + ) + }, + action = {}, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + OnboardingVisaChooseWallet( + state = OnboardingVisaChooseWalletUM( + options = persistentListOf( + SelectableChainRowUM( + event = OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet, + icon = R.drawable.ic_tangem_24, + text = TextReference.Str("Tangem Wallet"), + ), + SelectableChainRowUM( + event = OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet, + icon = R.drawable.ic_wallet_filled_24, + text = TextReference.Str("Other Wallet"), + ), + ), + selectedOption = SelectableChainRowUM( + event = OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet, + icon = R.drawable.ic_tangem_24, + text = TextReference.Str("Tangem Wallet"), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/OnboardingVisaChooseWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/OnboardingVisaChooseWalletUM.kt new file mode 100644 index 0000000000..0281417755 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/OnboardingVisaChooseWalletUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal data class OnboardingVisaChooseWalletUM( + val options: ImmutableList = persistentListOf(), + val selectedOption: SelectableChainRowUM? = null, + val onOptionSelected: (SelectableChainRowUM) -> Unit = {}, + val onContinueClick: () -> Unit = {}, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/SelectableChainRowUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/SelectableChainRowUM.kt new file mode 100644 index 0000000000..dee93cf95f --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/state/SelectableChainRowUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent + +internal data class SelectableChainRowUM( + val event: OnboardingVisaChooseWalletComponent.Params.Event, + @DrawableRes val icon: Int, + val text: TextReference, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt new file mode 100644 index 0000000000..6e8787f75a --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt @@ -0,0 +1,48 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.inprogress + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent +import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model.OnboardingVisaInProgressModel +import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.ui.OnboardingVisaInProgress +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +internal class OnboardingVisaInProgressComponent( + appComponentContext: AppComponentContext, + config: Config, + private val params: Params, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: OnboardingVisaInProgressModel = getOrCreateModel(config) + + init { + model.onDone + .onEach { params.onDone() } + .launchIn(componentScope) + } + + @Composable + override fun Content(modifier: Modifier) { + BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) + + OnboardingVisaInProgress( + modifier = modifier, + ) + } + + data class Config( + val scanResponse: ScanResponse, + ) + + data class Params( + val childParams: DefaultOnboardingVisaComponent.ChildParams, + val onDone: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt new file mode 100644 index 0000000000..94748a9a1a --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -0,0 +1,44 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.visa.model.VisaActivationRemoteState +import com.tangem.domain.visa.repository.VisaActivationRepository +import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ComponentScoped +internal class OnboardingVisaInProgressModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, +) : Model() { + + private val params = paramsContainer.require() + private val visaActivationRepository = visaActivationRepositoryFactory.create(params.scanResponse.card.cardId) + val onDone = MutableSharedFlow() + + init { + modelScope.launch { + while (true) { + val result = runCatching { + visaActivationRepository.getActivationRemoteStateLongPoll() + }.getOrNull() + + if (result == VisaActivationRemoteState.WaitingPinCode) { + onDone.emit(Unit) + break + } + + delay(timeMillis = 1000) + } + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/ui/OnboardingVisaInProgress.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/ui/OnboardingVisaInProgress.kt new file mode 100644 index 0000000000..d30e9a0b79 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/ui/OnboardingVisaInProgress.kt @@ -0,0 +1,37 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.inprogress.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +internal fun OnboardingVisaInProgress(modifier: Modifier = Modifier) { + Column( + modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + Column( + Modifier + .weight(1f) + .padding(top = 12.dp) + .padding(horizontal = 16.dp), + ) { + // TODO + } + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + OnboardingVisaInProgress() + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt new file mode 100644 index 0000000000..a9d2297b5f --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/OnboardingVisaOtherWalletComponent.kt @@ -0,0 +1,55 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent +import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model.OnboardingVisaOtherWalletModel +import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.OnboardingVisaOtherWallet +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +internal class OnboardingVisaOtherWalletComponent( + appComponentContext: AppComponentContext, + config: Config, + private val params: Params, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: OnboardingVisaOtherWalletModel = getOrCreateModel(config) + + init { + model.onDone + .onEach { params.onDone() } + .launchIn(componentScope) + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) + + OnboardingVisaOtherWallet( + modifier = modifier, + state = state, + ) + } + + data class Config( + val scanResponse: ScanResponse, + val visaDataForApprove: VisaDataForApprove, + ) + + data class Params( + val childParams: DefaultOnboardingVisaComponent.ChildParams, + val onDone: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt new file mode 100644 index 0000000000..51036ad803 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt @@ -0,0 +1,70 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.visa.model.VisaActivationRemoteState +import com.tangem.domain.visa.repository.VisaActivationRepository +import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.OnboardingVisaOtherWalletComponent +import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state.OnboardingVisaOtherWalletUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ComponentScoped +internal class OnboardingVisaOtherWalletModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val urlOpener: UrlOpener, + private val shareManager: ShareManager, + private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, +) : Model() { + + @Suppress("UnusedPrivateMember") + private val config = paramsContainer.require() + private val visaActivationRepository = visaActivationRepositoryFactory.create(config.scanResponse.card.cardId) + private val _uiState = MutableStateFlow(getInitialState()) + + val uiState = _uiState.asStateFlow() + val onDone = MutableSharedFlow() + + init { + modelScope.launch { + while (true) { + val result = runCatching { + visaActivationRepository.getActivationRemoteStateLongPoll() + }.getOrNull() + + if (result == VisaActivationRemoteState.WaitingPinCode) { + onDone.emit(Unit) + break + } + + delay(timeMillis = 1000) + } + } + } + + private fun getInitialState(): OnboardingVisaOtherWalletUM { + return OnboardingVisaOtherWalletUM( + onShareClick = ::onShareClicked, + onOpenInBrowserClick = ::onOpenInBrowserClicked, + ) + } + + private fun onShareClicked() { + shareManager.shareText("https://tangem.com/${config.visaDataForApprove.approveHash}") + } + + private fun onOpenInBrowserClicked() { + urlOpener.openUrl("https://tangem.com/${config.visaDataForApprove.approveHash}") + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt new file mode 100644 index 0000000000..4b0a8c2f3f --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt @@ -0,0 +1,137 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state.OnboardingVisaOtherWalletUM + +@Composable +internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modifier: Modifier = Modifier) { + Column( + modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + Box( + modifier = Modifier + .weight(1f) + .padding(top = 12.dp, bottom = 32.dp) + .padding(horizontal = 16.dp), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + IconBlock() + + SpacerH12() + + Text( + text = "Go to Website", + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + SpacerH12() + + Text( + text = "You will be able to complete your connection \n" + + "on the third-party web-site \n" + + "and back to the Tangem app", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } + } + + Column { + PrimaryButton( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + text = "Open in Browser", + onClick = state.onOpenInBrowserClick, + ) + + SpacerH12() + + SecondaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = "Share Link", + onClick = state.onOpenInBrowserClick, + ) + } + } +} + +@Composable +private fun IconBlock(modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + modifier = Modifier + .size(64.dp) + .background(TangemTheme.colors.button.primary, TangemTheme.shapes.roundedCorners8), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_tangem_24), + contentDescription = null, + tint = TangemTheme.colors.icon.primary2, + modifier = Modifier.size(38.dp), + ) + } + + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_loader), + contentDescription = null, + modifier = Modifier.width(32.dp), + ) + + Box( + modifier = Modifier + .size(64.dp) + .background(TangemTheme.colors.background.tertiary, TangemTheme.shapes.roundedCorners8), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_web_24), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier.size(48.dp), + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + OnboardingVisaOtherWallet(OnboardingVisaOtherWalletUM()) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/state/OnboardingVisaOtherWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/state/OnboardingVisaOtherWalletUM.kt new file mode 100644 index 0000000000..11895f4ca9 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/state/OnboardingVisaOtherWalletUM.kt @@ -0,0 +1,6 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state + +internal data class OnboardingVisaOtherWalletUM( + val onShareClick: () -> Unit = {}, + val onOpenInBrowserClick: () -> Unit = {}, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt new file mode 100644 index 0000000000..edf60844f7 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/OnboardingVisaPinCodeComponent.kt @@ -0,0 +1,55 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.pincode + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent +import com.tangem.features.onboarding.v2.visa.impl.child.pincode.model.OnboardingVisaPinCodeModel +import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.OnboardingVisaPinCode +import kotlinx.coroutines.launch + +internal class OnboardingVisaPinCodeComponent( + appComponentContext: AppComponentContext, + config: Config, + private val params: Params, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: OnboardingVisaPinCodeModel = getOrCreateModel(config) + + init { + componentScope.launch { + model.onDone.collect { params.onDone() } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) + + DisableScreenshotsDisposableEffect() + + OnboardingVisaPinCode( + modifier = modifier, + state = state, + ) + } + + data class Config( + val scanResponse: ScanResponse, + ) + + data class Params( + val childParams: DefaultOnboardingVisaComponent.ChildParams, + val onDone: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt new file mode 100644 index 0000000000..38f42fad17 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -0,0 +1,108 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.pincode.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.datasource.local.visa.VisaAuthTokenStorage +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent +import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@Stable +@ComponentScoped +internal class OnboardingVisaPinCodeModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletsListManager: UserWalletsListManager, + private val authTokenStorage: VisaAuthTokenStorage, + private val otpStorage: VisaAuthTokenStorage, +) : Model() { + + private val params = paramsContainer.require() + private val _uiState = MutableStateFlow(getInitialState()) + + val uiState = _uiState.asStateFlow() + val onDone = MutableSharedFlow() + + private fun getInitialState(): OnboardingVisaPinCodeUM { + return OnboardingVisaPinCodeUM( + onPinCodeChange = ::onPinCodeChange, + onSubmitClick = ::onSubmitClick, + ) + } + + private fun onPinCodeChange(pin: String) { + if (pin.all { it.isDigit() }) { + _uiState.update { + it.copy( + pinCode = pin, + submitButtonEnabled = checkPinCode(pin), + ) + } + } + } + + private fun onSubmitClick() { + if (checkPinCode(_uiState.value.pinCode).not()) return + + modelScope.launch { + loading(true) + + // TODO + // make backend call + saveWallet() + + loading(false) + } + } + + private fun checkPinCode(pin: String): Boolean { + return pin.length == PIN_CODE_LENGTH + } + + private fun loading(state: Boolean) { + _uiState.update { it.copy(submitButtonLoading = state) } + } + + private suspend fun saveWallet() { + val userWallet = createUserWallet(params.scanResponse) + userWalletsListManager.save(userWallet) + authTokenStorage.remove(params.scanResponse.card.cardId) + otpStorage.remove(params.scanResponse.card.cardId) + onDone.emit(Unit) + } + + private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet = withContext(dispatchers.io) { + val newActivationStatus = VisaCardActivationStatus.Activated( + visaAuthTokens = authTokenStorage.get(scanResponse.card.cardId) + ?: error("Impossible state. Wrong feature implementation"), + ) + + requireNotNull( + value = UserWalletBuilder( + scanResponse.copy(visaCardActivationStatus = newActivationStatus), + generateWalletNameUseCase, + ).build(), + lazyMessage = { "User wallet not created" }, + ) + } + + private companion object { + const val PIN_CODE_LENGTH = 4 + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt new file mode 100644 index 0000000000..c65e7afaeb --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt @@ -0,0 +1,200 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +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.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color.Companion.Transparent +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.rememberTextMeasurer +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.dp +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM +import kotlinx.coroutines.delay + +@Composable +internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 48.dp) + .padding(horizontal = 36.dp) + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Create\nPIN Code", + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + SpacerH16() + + Text( + text = "Set up a 4-digit code.\nIt will be used for payments.", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + + SpacerH(26.dp) + + PinCodeSection(state) + } + + NavigationPrimaryButton( + modifier = Modifier + .imePadding() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + primaryButton = NavigationButton( + textReference = TextReference.Str("Submit"), + onClick = state.onSubmitClick, + showProgress = state.submitButtonLoading, + isEnabled = state.submitButtonEnabled, + ), + ) + } +} + +@Composable +private fun PinCodeSection(state: OnboardingVisaPinCodeUM, modifier: Modifier = Modifier) { + val focusRequester = remember { FocusRequester() } + + PinCode( + modifier = modifier, + value = state.pinCode, + onValueChange = state.onPinCodeChange, + focusRequester = focusRequester, + ) + + LaunchedEffect(Unit) { + delay(timeMillis = 300) + focusRequester.requestFocus() + } +} + +@Composable +private fun PinCode( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + numbersCount: Int = 4, + focusRequester: FocusRequester = remember { FocusRequester() }, + onDone: ((String) -> Unit)? = null, +) { + val keyboardController = LocalSoftwareKeyboardController.current + + Box( + contentAlignment = Alignment.Center, + modifier = modifier, + ) { + BasicTextField( + value = value, + onValueChange = { text -> + if (text.length <= numbersCount) { + onValueChange(text) + if (text.length == numbersCount) { + onDone?.invoke(text) + } + } + }, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { + keyboardController?.hide() + },), + modifier = Modifier + .alpha(alpha = 0.01f) + .focusRequester(focusRequester), + textStyle = TextStyle.Default.copy(color = Transparent), + ) + + Row( + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { + keyboardController?.show() + focusRequester.requestFocus() + }, + ), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + repeat(numbersCount) { index -> + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.field.primary, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding( + vertical = 12.dp, + horizontal = 16.dp, + ), + contentAlignment = Alignment.Center, + ) { + Text( + modifier = Modifier.widthIn(min = measureTextWidth("0", TangemTheme.typography.h1)), + text = value.getOrNull(index)?.toString() ?: "", + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) + } + } + } + } +} + +@Composable +private fun measureTextWidth(text: String, style: TextStyle): Dp { + val textMeasurer = rememberTextMeasurer() + val widthInPixels = textMeasurer.measure(text, style).size.width + return with(LocalDensity.current) { widthInPixels.toDp() } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + OnboardingVisaPinCode( + state = OnboardingVisaPinCodeUM( + pinCode = "1234", + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt new file mode 100644 index 0000000000..9b17f93c5a --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state + +internal data class OnboardingVisaPinCodeUM( + val pinCode: String = "", + val onPinCodeChange: (String) -> Unit = {}, + val submitButtonLoading: Boolean = false, + val submitButtonEnabled: Boolean = true, + val onSubmitClick: () -> Unit = {}, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt index 0194489e69..95264e1199 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/OnboardingVisaWelcomeComponent.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent import com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.OnboardingVisaWelcome import com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state.OnboardingVisaWelcomeUM @@ -16,16 +17,29 @@ internal class OnboardingVisaWelcomeComponent( @Composable override fun Content(modifier: Modifier) { - BackHandler(onBack = remember(this) { { params.onBack() } }) + val state = remember(this) { + OnboardingVisaWelcomeUM( + mode = if (params.isWelcomeBack) { + OnboardingVisaWelcomeUM.Mode.WelcomeBack + } else { + OnboardingVisaWelcomeUM.Mode.Hello + }, + userName = "User", // TODO: + onContinueClick = params.onDone, + ) + } + + BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) OnboardingVisaWelcome( modifier = modifier, - state = OnboardingVisaWelcomeUM(), + state = state, ) } data class Params( - val onBack: () -> Unit, - val startActivation: () -> Unit, + val childParams: DefaultOnboardingVisaComponent.ChildParams, + val isWelcomeBack: Boolean, + val onDone: () -> Unit, ) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt index 61c2b6e0d8..d21c1a2f43 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt @@ -1,14 +1,17 @@ package com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SpacerH @@ -87,30 +90,36 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod } } +@Suppress("MagicNumber") @Composable fun Artwork(modifier: Modifier = Modifier) { Box(modifier) { val circleColor = TangemTheme.colors.background.secondary + var cardHeightPx by remember { mutableIntStateOf(0) } + val circleRadiusPx = cardHeightPx / 2 + cardHeightPx / 8f + val circleSize = with(LocalDensity.current) { (circleRadiusPx * 2).toDp() } Canvas( modifier = Modifier .align(Alignment.Center) - .size(254.dp) + .size(circleSize) .fillMaxSize(), ) { drawCircle( color = circleColor, - radius = size.width / 2, + radius = circleRadiusPx, center = center, ) } - AsyncImage( - model = R.drawable.img_card_visa, + Image( + painter = painterResource(R.drawable.img_card_visa), contentDescription = null, - modifier = Modifier + modifier = modifier .align(Alignment.Center) - .size(128.dp), // Adjust the size as needed + .onSizeChanged { cardHeightPx = it.height } + .widthIn(max = 512.dp) + .fillMaxWidth(), ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt index abb9a0cecf..5ee550c847 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/di/ComponentModule.kt @@ -1,11 +1,22 @@ package com.tangem.features.onboarding.v2.visa.impl.di +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent import com.tangem.features.onboarding.v2.visa.impl.DefaultOnboardingVisaComponent +import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model.OnboardingVisaAccessCodeModel +import com.tangem.features.onboarding.v2.visa.impl.child.approve.model.OnboardingVisaApproveModel +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model.OnboardingVisaChooseWalletModel +import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model.OnboardingVisaInProgressModel +import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model.OnboardingVisaOtherWalletModel +import com.tangem.features.onboarding.v2.visa.impl.child.pincode.model.OnboardingVisaPinCodeModel +import com.tangem.features.onboarding.v2.visa.impl.model.OnboardingVisaModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import javax.inject.Singleton @Module @@ -15,4 +26,44 @@ internal interface ComponentModule { @Binds @Singleton fun bindComponentFactory(factory: DefaultOnboardingVisaComponent.Factory): OnboardingVisaComponent.Factory +} + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(OnboardingVisaAccessCodeModel::class) + fun provideModel(model: OnboardingVisaAccessCodeModel): Model + + @Binds + @IntoMap + @ClassKey(OnboardingVisaChooseWalletModel::class) + fun provideModel2(model: OnboardingVisaChooseWalletModel): Model + + @Binds + @IntoMap + @ClassKey(OnboardingVisaPinCodeModel::class) + fun provideModel3(model: OnboardingVisaPinCodeModel): Model + + @Binds + @IntoMap + @ClassKey(OnboardingVisaApproveModel::class) + fun provideModel4(model: OnboardingVisaApproveModel): Model + + @Binds + @IntoMap + @ClassKey(OnboardingVisaOtherWalletModel::class) + fun provideModel5(model: OnboardingVisaOtherWalletModel): Model + + @Binds + @IntoMap + @ClassKey(OnboardingVisaInProgressModel::class) + fun provideModel6(model: OnboardingVisaInProgressModel): Model + + @Binds + @IntoMap + @ClassKey(OnboardingVisaModel::class) + fun provideModel7(model: OnboardingVisaModel): Model } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt new file mode 100644 index 0000000000..f1eab1d0d0 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt @@ -0,0 +1,136 @@ +package com.tangem.features.onboarding.v2.visa.impl.model + +import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.visa.model.VisaActivationRemoteState +import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent +import com.tangem.features.onboarding.v2.visa.impl.OnboardingVisaInnerNavigationState +import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent +import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent +import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute +import com.tangem.features.onboarding.v2.visa.impl.route.stepNum +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@Stable +@ComponentScoped +internal class OnboardingVisaModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + @Suppress("UnusedPrivateMember") + private val params = paramsContainer.require() + + private val _currentScanResponse = MutableStateFlow(params.scanResponse) + + val initialRoute = initializeRoute() + + val stackNavigation = StackNavigation() + + private val _innerNavigationState = + MutableStateFlow(OnboardingVisaInnerNavigationState(stackSize = initialRoute.stepNum())) + + val currentScanResponse = _currentScanResponse.asStateFlow() + + val innerNavigationState = _innerNavigationState.asStateFlow() + + fun updateStepForNewRoute(route: OnboardingVisaRoute) { + _innerNavigationState.value = innerNavigationState.value.copy( + stackSize = route.stepNum(), + ) + } + + fun navigateFromWelcome(route: OnboardingVisaRoute.Welcome) { + if (route.isWelcomeBack) { + val scanResponse = _currentScanResponse.value + val activationStatus = scanResponse.visaCardActivationStatus as? VisaCardActivationStatus + .ActivationStarted ?: error("Activation status is not correct for welcome back route") + + when (activationStatus.remoteState) { + VisaActivationRemoteState.CardWalletSignatureRequired, + VisaActivationRemoteState.CustomerWalletSignatureRequired, + -> OnboardingVisaRoute.AccessCode + VisaActivationRemoteState.WaitingPinCode -> OnboardingVisaRoute.PinCode + VisaActivationRemoteState.WaitingForActivationFinishing -> OnboardingVisaRoute.InProgress + VisaActivationRemoteState.PaymentAccountDeploying -> OnboardingVisaRoute.InProgress + else -> error("Remote state is not correct for welcome back route") + } + } else { + stackNavigation.push(OnboardingVisaRoute.AccessCode) + } + } + + fun navigateFromAccessCode(result: OnboardingVisaAccessCodeComponent.DoneEvent) { + _currentScanResponse.value = result.newScanResponse + stackNavigation.push( + if (result.walletFound) { + OnboardingVisaRoute.TangemWalletApproveOption( + visaDataForApprove = result.visaDataForApprove, + allowNavigateBack = false, + ) + } else { + OnboardingVisaRoute.ChooseWallet(result.visaDataForApprove) + }, + ) + } + + fun navigateFromChooseWallet( + route: OnboardingVisaRoute.ChooseWallet, + event: OnboardingVisaChooseWalletComponent.Params.Event, + ) { + stackNavigation.push( + when (event) { + OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet -> + OnboardingVisaRoute.TangemWalletApproveOption( + visaDataForApprove = route.visaDataForApprove, + allowNavigateBack = true, + ) + OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet -> + OnboardingVisaRoute.OtherWalletApproveOption(route.visaDataForApprove) + }, + ) + } + + fun onChildBack(currentRoute: OnboardingVisaRoute, lastRoute: Boolean) { + if (lastRoute) { + // TODO show dialog + return + } + + when (currentRoute) { + OnboardingVisaRoute.AccessCode, + is OnboardingVisaRoute.OtherWalletApproveOption, + -> stackNavigation.pop() + is OnboardingVisaRoute.TangemWalletApproveOption -> { + if (currentRoute.allowNavigateBack) { + stackNavigation.pop() + } + } + is OnboardingVisaRoute.Welcome, + is OnboardingVisaRoute.ChooseWallet, + OnboardingVisaRoute.InProgress, + OnboardingVisaRoute.PinCode, + -> { + } + } + } + + private fun initializeRoute(): OnboardingVisaRoute { + val activationStatus = params.scanResponse.visaCardActivationStatus + + return when (activationStatus) { + is VisaCardActivationStatus.ActivationStarted -> OnboardingVisaRoute.Welcome(isWelcomeBack = true) + is VisaCardActivationStatus.NotStartedActivation -> OnboardingVisaRoute.Welcome(isWelcomeBack = false) + else -> error("Visa activation status is not correct for onboarding flow") + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt new file mode 100644 index 0000000000..64f7576d0f --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/OnboardingVisaRoute.kt @@ -0,0 +1,32 @@ +package com.tangem.features.onboarding.v2.visa.impl.route + +import com.tangem.domain.visa.model.VisaDataForApprove +import kotlinx.serialization.Serializable + +@Serializable +sealed class OnboardingVisaRoute { + + @Serializable + data class Welcome(val isWelcomeBack: Boolean) : OnboardingVisaRoute() + + @Serializable + data object AccessCode : OnboardingVisaRoute() + + @Serializable + data class ChooseWallet(val visaDataForApprove: VisaDataForApprove) : OnboardingVisaRoute() + + @Serializable + data class TangemWalletApproveOption( + val visaDataForApprove: VisaDataForApprove, + val allowNavigateBack: Boolean, + ) : OnboardingVisaRoute() + + @Serializable + data class OtherWalletApproveOption(val visaDataForApprove: VisaDataForApprove) : OnboardingVisaRoute() + + @Serializable + data object InProgress : OnboardingVisaRoute() + + @Serializable + data object PinCode : OnboardingVisaRoute() +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Step.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Step.kt new file mode 100644 index 0000000000..a4deb7fcdb --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Step.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onboarding.v2.visa.impl.route + +@Suppress("MagicNumber") +internal fun OnboardingVisaRoute.stepNum() = when (this) { + is OnboardingVisaRoute.Welcome -> 1 + OnboardingVisaRoute.AccessCode -> 2 + is OnboardingVisaRoute.ChooseWallet -> 3 + is OnboardingVisaRoute.OtherWalletApproveOption -> 4 + is OnboardingVisaRoute.TangemWalletApproveOption -> 4 + OnboardingVisaRoute.InProgress -> 5 + OnboardingVisaRoute.PinCode -> 6 +} + +internal const val ONBOARDING_VISA_STEPS_COUNT = 7 \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt new file mode 100644 index 0000000000..89e6c7a044 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onboarding.v2.visa.impl.route + +import com.tangem.core.ui.extensions.TextReference + +// TODO add translations [REDACTED_TASK_KEY] +internal fun OnboardingVisaRoute.screenTitle(): TextReference = when (this) { + OnboardingVisaRoute.AccessCode -> TextReference.Str("Access code") + is OnboardingVisaRoute.ChooseWallet -> TextReference.Str("Account activation") + OnboardingVisaRoute.InProgress -> TextReference.Str("In progress") + is OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection") + OnboardingVisaRoute.PinCode -> TextReference.Str("PIN code") + is OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection") + is OnboardingVisaRoute.Welcome -> TextReference.Str("Getting started") +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt new file mode 100644 index 0000000000..40a9a8597d --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt @@ -0,0 +1,9 @@ +package com.tangem.features.onboarding.v2.visa.impl.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +internal fun OnboardingVisa(modifier: Modifier = Modifier, childContent: @Composable (Modifier) -> Unit) { + childContent(modifier) +} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt index 6db1217109..51d9740d8c 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt @@ -1,10 +1,14 @@ package com.tangem.feature.onboarding.api import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.* -import androidx.compose.material.LinearProgressIndicator +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState import com.tangem.feature.onboarding.presentation.wallet2.ui.* @@ -19,31 +23,22 @@ class OnboardingSeedPhraseScreen : OnboardingSeedPhraseApi { override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) { BackHandler(onBack = uiState.onBackClick) Column { - ProgressIndicator(progress) + TangemLinearProgressIndicator( + progress = { progress }, + color = TangemTheme.colors.icon.primary1, + backgroundColor = TangemTheme.colors.icon.primary1.copy(alpha = 0.4f), + strokeCap = StrokeCap.Round, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = TangemTheme.dimens.size8) + .padding(horizontal = TangemTheme.dimens.size16), + ) Content(subScreen, uiState) } } } -@Composable -private fun ProgressIndicator(progress: Float) { - Box( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(top = TangemTheme.dimens.size8), - ) { - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.size16), - color = TangemTheme.colors.background.action, - backgroundColor = TangemTheme.colors.background.action.copy(alpha = 0.081f), - progress = progress, - ) - } -} - @Composable private fun Content(screen: SeedPhraseScreen, uiState: OnboardingSeedPhraseState) { when (screen) { diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt index 7d46cb6ad1..9202fd9fc8 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt @@ -3,4 +3,6 @@ package com.tangem.features.onramp interface OnrampFeatureToggles { val isFeatureEnabled: Boolean + + val isHotTokensEnabled: Boolean } \ No newline at end of file diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index ea3b825ef0..2df9a81756 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.card) implementation(projects.domain.demo) implementation(projects.domain.legacy) implementation(projects.domain.models) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt index 337eb159c5..5a0c6f5a15 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt @@ -7,5 +7,8 @@ internal class DefaultOnrampFeatureToggles( ) : OnrampFeatureToggles { override val isFeatureEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("ONRAMP_ENABLED") + get() = featureTogglesManager.isFeatureEnabled(name = "ONRAMP_ENABLED") + + override val isHotTokensEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "ONRAMP_HOT_TOKENS_ENABLED") } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/DefaultOnrampAddToPortfolioComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/DefaultOnrampAddToPortfolioComponent.kt new file mode 100644 index 0000000000..aad77fc333 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/DefaultOnrampAddToPortfolioComponent.kt @@ -0,0 +1,53 @@ +package com.tangem.features.onramp.hottokens.portfolio + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.onramp.hottokens.portfolio.model.OnrampAddToPortfolioModel +import com.tangem.features.onramp.hottokens.portfolio.ui.OnrampAddToPortfolioBottomSheet +import com.tangem.features.onramp.hottokens.portfolio.ui.OnrampAddToPortfolioContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnrampAddToPortfolioComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: OnrampAddToPortfolioComponent.Params, +) : OnrampAddToPortfolioComponent, AppComponentContext by context { + + private val model: OnrampAddToPortfolioModel = getOrCreateModel(params) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.state.collectAsStateWithLifecycle() + val bottomSheetConfig = remember(key1 = this) { + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + + OnrampAddToPortfolioBottomSheet( + config = bottomSheetConfig, + content = { OnrampAddToPortfolioContent(state = state) }, + ) + } + + @AssistedFactory + interface Factory : OnrampAddToPortfolioComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnrampAddToPortfolioComponent.Params, + ): DefaultOnrampAddToPortfolioComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/OnrampAddToPortfolioComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/OnrampAddToPortfolioComponent.kt new file mode 100644 index 0000000000..268e140a96 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/OnrampAddToPortfolioComponent.kt @@ -0,0 +1,34 @@ +package com.tangem.features.onramp.hottokens.portfolio + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Onramp bottom sheet component for adding token to portfolio + * +[REDACTED_AUTHOR] + */ +interface OnrampAddToPortfolioComponent : ComposableBottomSheetComponent { + + /** + * Params + * + * @property userWalletId user wallet id + * @property cryptoCurrency crypto currency + * @property currencyIconState currency icon state + * @property onSuccessAdding lambda be invoked when token will added + * @property onDismiss lambda be invoked when bottom sheet is dismissed + */ + data class Params( + val userWalletId: UserWalletId, + val cryptoCurrency: CryptoCurrency, + val currencyIconState: CurrencyIconState, + val onSuccessAdding: () -> Unit, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/di/OnrampAddToPortfolioComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/di/OnrampAddToPortfolioComponentModule.kt new file mode 100644 index 0000000000..57d2660750 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/di/OnrampAddToPortfolioComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.hottokens.portfolio.di + +import com.tangem.features.onramp.hottokens.portfolio.DefaultOnrampAddToPortfolioComponent +import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampAddToPortfolioComponentModule { + + @Binds + @Singleton + fun bindOnrampAddToPortfolioComponentFactory( + factory: DefaultOnrampAddToPortfolioComponent.Factory, + ): OnrampAddToPortfolioComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/di/OnrampAddToPortfolioModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/di/OnrampAddToPortfolioModelModule.kt new file mode 100644 index 0000000000..086a66b0d0 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/di/OnrampAddToPortfolioModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.hottokens.portfolio.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.hottokens.portfolio.model.OnrampAddToPortfolioModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampAddToPortfolioModelModule { + + @Binds + @IntoMap + @ClassKey(OnrampAddToPortfolioModel::class) + fun bindOnrampSelectCountryModel(model: OnrampAddToPortfolioModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddToPortfolioBSConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddToPortfolioBSConfig.kt new file mode 100644 index 0000000000..5136e3709e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddToPortfolioBSConfig.kt @@ -0,0 +1,13 @@ +package com.tangem.features.onramp.hottokens.portfolio.entity + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.domain.tokens.model.CryptoCurrency + +internal sealed class OnrampAddToPortfolioBSConfig { + + data class AddToPortfolio( + val cryptoCurrency: CryptoCurrency, + val currencyIconState: CurrencyIconState, + val onSuccessAdding: () -> Unit, + ) : OnrampAddToPortfolioBSConfig() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddToPortfolioUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddToPortfolioUM.kt new file mode 100644 index 0000000000..44a2c35c1e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddToPortfolioUM.kt @@ -0,0 +1,34 @@ +package com.tangem.features.onramp.hottokens.portfolio.entity + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.onramp.impl.R + +/** + * Onramp add to portfolio UM + * + * @property currencyName currency name + * @property networkName network name + * @property currencyIconState currency icon state + * @property onAddClick lambda be invoked when add button is clicked + * +[REDACTED_AUTHOR] + */ +data class OnrampAddToPortfolioUM( + val currencyName: String, + val networkName: String, + val currencyIconState: CurrencyIconState, + val onAddClick: () -> Unit, +) { + + val bsTitle: TextReference = resourceReference(id = R.string.common_add_token) + + val subtitle: TextReference = resourceReference( + id = R.string.hot_crypto_token_network, + formatArgs = wrappedList(networkName), + ) + + val addButtonText: TextReference = resourceReference(R.string.common_add_to_portfolio) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt new file mode 100644 index 0000000000..83470a0ffa --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt @@ -0,0 +1,63 @@ +package com.tangem.features.onramp.hottokens.portfolio.model + +import arrow.core.getOrElse +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase +import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent +import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddToPortfolioUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +/** + * Model for adding token to portfolio + * + * @param paramsContainer params container + * @property dispatchers dispatchers + * @property derivePublicKeysUseCase use case for deriving public key + * @property addCryptoCurrenciesUseCase use case for adding crypto currency + * +[REDACTED_AUTHOR] + */ +@ComponentScoped +internal class OnrampAddToPortfolioModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val derivePublicKeysUseCase: DerivePublicKeysUseCase, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, +) : Model() { + + val state: StateFlow get() = _state + private val _state = MutableStateFlow(value = getInitialState()) + + private val params: OnrampAddToPortfolioComponent.Params = paramsContainer.require() + + private fun getInitialState(): OnrampAddToPortfolioUM { + return OnrampAddToPortfolioUM( + currencyName = params.cryptoCurrency.name, + networkName = params.cryptoCurrency.network.name, + currencyIconState = params.currencyIconState, + onAddClick = ::onAddClick, + ) + } + + private fun onAddClick() { + modelScope.launch { + derivePublicKeysUseCase(params.userWalletId, listOfNotNull(params.cryptoCurrency)).getOrElse { + Timber.e("Failed to derive public keys: $it") + } + + addCryptoCurrenciesUseCase( + userWalletId = params.userWalletId, + currencies = listOfNotNull(params.cryptoCurrency), + ) + .onRight { params.onSuccessAdding() } + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/ui/OnrampAddToPortfolioBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/ui/OnrampAddToPortfolioBottomSheet.kt new file mode 100644 index 0000000000..253dd52eed --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/ui/OnrampAddToPortfolioBottomSheet.kt @@ -0,0 +1,17 @@ +package com.tangem.features.onramp.hottokens.portfolio.ui + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun OnrampAddToPortfolioBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { + TangemBottomSheet( + config = config, + addBottomInsets = true, + containerColor = TangemTheme.colors.background.tertiary, + content = { content() }, + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/ui/OnrampAddToPortfolioContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/ui/OnrampAddToPortfolioContent.kt new file mode 100644 index 0000000000..d57182b16b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/ui/OnrampAddToPortfolioContent.kt @@ -0,0 +1,162 @@ +package com.tangem.features.onramp.hottokens.portfolio.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddToPortfolioUM +import com.tangem.features.onramp.impl.R + +/** + * Onramp add to portfolio content + * + * @param state state + */ +@Composable +fun OnrampAddToPortfolioContent(state: OnrampAddToPortfolioUM) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TopAppBar(text = state.bsTitle) + + SpacerH(height = 19.dp) + + CurrencyIcon(state = state.currencyIconState) + + SpacerH12() + + CurrencyName( + text = state.currencyName, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + SpacerH(height = 6.dp) + + CurrencyNetworkName( + text = state.subtitle, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + SpacerH24() + + AddToPortfolioButton( + text = state.addButtonText, + onClick = state.onAddClick, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + SpacerH16() + } +} + +@Composable +private fun TopAppBar(text: TextReference) { + TangemTopAppBar( + title = text.resolveReference(), + endButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_information_24, + onIconClicked = {}, + ), + iconTint = TangemTheme.colors.icon.informative, + titleAlignment = Alignment.CenterHorizontally, + height = TangemTopAppBarHeight.BOTTOM_SHEET, + ) +} + +@Composable +private fun CurrencyName(text: String, modifier: Modifier = Modifier) { + Text( + text = text, + modifier = modifier, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + style = TangemTheme.typography.subtitle1, + ) +} + +@Composable +private fun CurrencyNetworkName(text: TextReference, modifier: Modifier = Modifier) { + Text( + text = text.resolveReference(), + modifier = modifier, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + style = TangemTheme.typography.body2, + ) +} + +@Composable +private fun AddToPortfolioButton(text: TextReference, onClick: () -> Unit, modifier: Modifier = Modifier) { + TangemButton( + text = text.resolveReference(), + icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), + onClick = onClick, + colors = TangemButtonsDefaults.primaryButtonColors, + showProgress = false, + enabled = true, + modifier = modifier.fillMaxWidth(), + size = TangemButtonSize.WideAction, + textStyle = TangemTheme.typography.subtitle1, + ) +} + +@Preview +@Composable +private fun PreviewOnrampAddToPortfolioContent() { + TangemThemePreview { + val bottomSheetConfig = remember { + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ) + } + OnrampAddToPortfolioBottomSheet( + config = bottomSheetConfig, + content = { + OnrampAddToPortfolioContent( + state = OnrampAddToPortfolioUM( + currencyName = "Tether", + networkName = "Ethereum", + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_eth_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + showCustomBadge = false, + ), + onAddClick = {}, + ), + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/proguard-rules.pro b/features/qr-scanning/impl/proguard-rules.pro deleted file mode 100644 index 481bb43481..0000000000 --- a/features/qr-scanning/impl/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index 709bd1f9bc..e31d57f978 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -229,7 +229,7 @@ internal class FeeStateFactory( symbol = this.token.symbol, contractAddress = this.token.contractAddress, decimals = this.token.decimals, - id = this.token.id.rawCurrencyId, + id = this.token.id.rawCurrencyId?.value, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt index 5decf65514..fdc4685953 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -114,7 +114,7 @@ internal class StakingAnalyticSender( return when (value.actionType) { StakingActionCommonType.Enter -> StakingActionType.STAKE - StakingActionCommonType.Exit -> StakingActionType.UNSTAKE + is StakingActionCommonType.Exit -> StakingActionType.UNSTAKE is StakingActionCommonType.Pending -> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index d7e8d24c25..60afd5cfef 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -2,10 +2,9 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.analytics.StakingAnalyticsEvent +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender -import com.tangem.lib.crypto.BlockchainUtils.isSolana internal class StakingStateRouter( private val appRouter: AppRouter, @@ -23,10 +22,9 @@ internal class StakingStateRouter( fun onNextClick() { when (stateController.value.currentStep) { - StakingStep.InitialInfo -> when (stateController.value.actionType) { + StakingStep.InitialInfo -> when (val actionType = stateController.value.actionType) { StakingActionCommonType.Enter -> showAmount() - // TODO staking [REDACTED_TASK_KEY] support solana multisize hashes signing - StakingActionCommonType.Exit -> if (isSolana(stateController.value.cryptoCurrencyBlockchainId)) { + is StakingActionCommonType.Exit -> if (actionType.partiallyUnstakeDisabled) { showConfirmation() } else { showAmount() @@ -54,15 +52,12 @@ internal class StakingStateRouter( StakingStep.Amount, -> showInitial() StakingStep.Confirmation -> { - val isEnter = uiState.actionType == StakingActionCommonType.Enter - val isExit = uiState.actionType == StakingActionCommonType.Exit - - // TODO staking [REDACTED_TASK_KEY] support solana multisize hashes signing - val isSolana = isSolana(uiState.cryptoCurrencyBlockchainId) - if (isEnter || isExit && !isSolana) { - showAmount() - } else { - showInitial() + when (val actionType = uiState.actionType) { + StakingActionCommonType.Enter -> showAmount() + is StakingActionCommonType.Pending -> showInitial() + is StakingActionCommonType.Exit -> { + if (actionType.partiallyUnstakeDisabled) showInitial() else showAmount() + } } } StakingStep.Validators -> showConfirmation() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index c8c0be08e6..604fbe7497 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -8,7 +8,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType @@ -120,6 +120,7 @@ internal sealed class StakingStates { val footerText: TextReference, val transactionDoneState: TransactionDoneState, val isApprovalNeeded: Boolean, + val isAmountEditable: Boolean, val allowance: BigDecimal, val reduceAmountBy: BigDecimal?, ) : ConfirmationState() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index b151140d55..d9996e2ded 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -89,7 +89,7 @@ internal class RewardsValidatorStateConverter( formattedCryptoAmount = cryptoAmount, fiatAmount = fiatValue, formattedFiatAmount = formattedFiatAmount, - rawCurrencyId = cryptoCurrency.id.rawCurrencyId, + rawCurrencyId = cryptoCurrency.id.rawCurrencyId?.value, pendingActions = balance.pendingActions.toPersistentList(), isClickable = true, type = balance.type, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt index 3d384541bd..b2c2a93ed7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt @@ -41,4 +41,11 @@ internal sealed class StakingAlertUM : AlertUM { override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title) override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) } + + data object ValidatorsUnavailable : StakingAlertUM() { + override val onConfirmClick: (() -> Unit)? = null + override val title: TextReference = resourceReference(id = R.string.staking_error_no_validators_title) + override val message: TextReference = resourceReference(id = R.string.staking_error_no_validators_message) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt index 75bcbd3efd..6d54195e71 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt @@ -41,4 +41,9 @@ internal class StakingEventFactory( ) stateController.updateEvent(alert) } + + fun createStakingValidatorsUnavailableAlert() { + val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.ValidatorsUnavailable) + stateController.updateEvent(alert) + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt index 9167845687..6ed1669b40 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt @@ -15,6 +15,7 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.staking.getCurrentToken import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.wallets.models.UserWallet diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt index 09b6415c41..1de6d402d2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -18,6 +18,7 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.staking.getCurrentToken import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index ac86e7084a..15d569833c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -48,5 +48,6 @@ internal object ConfirmationStatePreviewData { allowance = BigDecimal.ZERO, reduceAmountBy = null, pendingActions = null, + isAmountEditable = true, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 9bc454ab92..99c1cb3379 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.previewdata import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.Yield diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt index 532054c768..736d2d3fa2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt @@ -26,7 +26,7 @@ internal class SetAmountDataTransformer( private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) override fun transform(prevState: StakingUiState): StakingUiState { - val title = if (prevState.actionType == StakingActionCommonType.Exit) { + val title = if (prevState.actionType is StakingActionCommonType.Exit) { resourceReference(R.string.staking_staked_amount) } else { stringReference(userWalletProvider().name) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index e989a47670..61eb924114 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -133,7 +133,7 @@ internal class SetButtonsStateTransformer( resourceReference(R.string.common_stake) } } - StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake) + is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake) is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt index 88ca568904..d9d1354ee5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt @@ -3,17 +3,21 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions import com.tangem.features.staking.impl.presentation.state.utils.isTronStakedBalance +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.extensions.isPositive import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal +@Suppress("LongParameterList") internal class SetConfirmationStateInitTransformer( private val isEnter: Boolean, private val isExplicitExit: Boolean, @@ -21,9 +25,9 @@ internal class SetConfirmationStateInitTransformer( private val stakingApproval: StakingApproval, private val stakingAllowance: BigDecimal, private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val yieldArgs: Yield.Args, private val pendingActions: ImmutableList? = null, private val pendingAction: PendingAction? = pendingActions?.firstOrNull(), - ) : Transformer { private val networkId @@ -41,10 +45,10 @@ internal class SetConfirmationStateInitTransformer( override fun transform(prevState: StakingUiState): StakingUiState { val actionType = when { isEnter -> StakingActionCommonType.Enter - isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit + isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit(isPartiallyUnstakeDisabled(prevState)) else -> when (pendingAction?.type) { StakingActionType.STAKE -> StakingActionCommonType.Enter - StakingActionType.UNSTAKE -> StakingActionCommonType.Exit + StakingActionType.UNSTAKE -> StakingActionCommonType.Exit(isPartiallyUnstakeDisabled(prevState)) StakingActionType.CLAIM_REWARDS, StakingActionType.RESTAKE_REWARDS, -> StakingActionCommonType.Pending.Rewards @@ -67,10 +71,26 @@ internal class SetConfirmationStateInitTransformer( transactionDoneState = TransactionDoneState.Empty, isApprovalNeeded = stakingApproval is StakingApproval.Needed, allowance = stakingAllowance, + isAmountEditable = actionType == StakingActionCommonType.Enter || + actionType is StakingActionCommonType.Exit && + !actionType.partiallyUnstakeDisabled, reduceAmountBy = null, pendingAction = pendingAction, pendingActions = pendingActions.takeIf { isComposePendingActions }, ), ) } + + private fun isPartiallyUnstakeDisabled(state: StakingUiState): Boolean { + // TODO staking [REDACTED_TASK_KEY] support solana multisize hashes signing + if (BlockchainUtils.isSolana(state.cryptoCurrencyBlockchainId)) { + return true + } + + val exitArgs = yieldArgs.exit ?: return false + val exitAmount = exitArgs.args[Yield.Args.ArgType.AMOUNT] ?: return false + val min = exitAmount.minimum ?: return false + val max = exitAmount.maximum ?: return false + return !min.isPositive() && !max.isPositive() + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 6825aeb4f6..1e317fafab 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.Yield diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt index a29c1a8a85..46258e60a7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt @@ -35,7 +35,7 @@ internal object SetTitleTransformer : Transformer { R.string.staking_title_stake, wrappedList(prevState.cryptoCurrencyName), ) - StakingActionCommonType.Exit -> resourceReference( + is StakingActionCommonType.Exit -> resourceReference( R.string.staking_title_unstake, wrappedList(prevState.cryptoCurrencyName), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt index 41fdc707cf..7ff0dc011e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -20,7 +20,7 @@ internal class AmountChangeStateTransformer( override fun transform(prevState: StakingUiState): StakingUiState { val actionType = prevState.actionType - val maxEnterAmount = if (actionType == StakingActionCommonType.Exit) { + val maxEnterAmount = if (actionType is StakingActionCommonType.Exit) { EnterAmountBoundary( amount = prevState.balanceState?.cryptoAmount, fiatAmount = prevState.balanceState?.fiatAmount, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt index db2865b4ec..aee3d92b78 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -19,7 +19,7 @@ internal class AmountMaxValueStateTransformer( private val maxEnterAmountConverter = MaxEnterAmountConverter() override fun transform(prevState: StakingUiState): StakingUiState { - val maxEnterAmount = if (actionType == StakingActionCommonType.Exit) { + val maxEnterAmount = if (actionType is StakingActionCommonType.Exit) { EnterAmountBoundary( amount = prevState.balanceState?.cryptoAmount, fiatAmount = prevState.balanceState?.fiatAmount, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index d890ab712a..655b39d571 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -61,7 +61,7 @@ internal class AmountRequirementStateTransformer( R.string.staking_amount_tron_integer_error, wrappedList(roundedDownCrypto), ) - StakingActionCommonType.Exit -> resourceReference( + is StakingActionCommonType.Exit -> resourceReference( R.string.staking_amount_tron_integer_error_unstaking, wrappedList(roundedDownCrypto), ) @@ -96,7 +96,7 @@ internal class AmountRequirementStateTransformer( val enterRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT] enterRequirements?.getError(amountDecimal, R.string.staking_amount_requirement_error) } - StakingActionCommonType.Exit -> { + is StakingActionCommonType.Exit -> { val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT) exitRequirements?.getError(amountDecimal, R.string.staking_unstake_amount_requirement_error) } @@ -107,7 +107,7 @@ internal class AmountRequirementStateTransformer( private fun isIntegerOnlyError(amountState: AmountState.Data, actionType: StakingActionCommonType): Boolean { val cryptoAmountValue = amountState.amountTextField.cryptoAmount.value ?: return false - val isEnterOrExit = actionType == StakingActionCommonType.Enter || actionType == StakingActionCommonType.Exit + val isEnterOrExit = actionType == StakingActionCommonType.Enter || actionType is StakingActionCommonType.Exit val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value) val isIntegerOnly = cryptoAmountValue.isZero() || cryptoAmountValue.remainder(BigDecimal.ONE).isZero() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt index 1a527dd242..aed53b2408 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt @@ -45,7 +45,7 @@ internal class StakingInfoNotificationsFactory( when (prevState.actionType) { StakingActionCommonType.Enter -> addEnterInfoNotifications(sendingAmount, feeValue) - StakingActionCommonType.Exit -> addExitInfoNotifications() + is StakingActionCommonType.Exit -> addExitInfoNotifications() is StakingActionCommonType.Pending -> addPendingInfoNotifications(prevState) } } @@ -167,7 +167,7 @@ internal class StakingInfoNotificationsFactory( prevState: StakingUiState, actionAmount: BigDecimal, ) { - if (prevState.actionType != StakingActionCommonType.Exit) return + if (prevState.actionType !is StakingActionCommonType.Exit) return val maxAmount = prevState.balanceState?.cryptoAmount ?: return val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT) ?: return diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 2df8cdd525..765480203c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -18,7 +18,6 @@ import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingNotification @@ -39,11 +38,8 @@ internal fun StakingConfirmationContent( state: StakingStates.ConfirmationState, validatorState: StakingStates.ValidatorState, clickIntents: StakingClickIntents, - type: StakingActionCommonType, - isSolana: Boolean, // TODO staking [REDACTED_TASK_KEY] support solana multisize hashes signing ) { if (state !is StakingStates.ConfirmationState.Data) return - val isAmountEditable = type == StakingActionCommonType.Enter || type == StakingActionCommonType.Exit && !isSolana val isTransactionSent = state.innerState == InnerConfirmationStakingState.COMPLETED val isTransactionInProgress = state.notifications.any { it is StakingNotification.Warning.TransactionInProgress } Column( @@ -65,8 +61,8 @@ internal fun StakingConfirmationContent( } AmountBlock( amountState = amountState, - isClickDisabled = !isAmountEditable || isTransactionSent || isTransactionInProgress, - isEditingDisabled = !isAmountEditable && state.innerState != InnerConfirmationStakingState.COMPLETED, + isClickDisabled = !state.isAmountEditable || isTransactionSent || isTransactionInProgress, + isEditingDisabled = !state.isAmountEditable && state.innerState != InnerConfirmationStakingState.COMPLETED, onClick = clickIntents::onPrevClick, ) ValidatorBlock( @@ -90,8 +86,6 @@ private fun Preview_StakingConfirmationContent() { state = ConfirmationStatePreviewData.assentStakingState, validatorState = ValidatorStatePreviewData.validatorState, clickIntents = StakingClickIntentsStub, - type = StakingActionCommonType.Enter, - isSolana = false, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index d213a829f3..63d2aacc70 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -10,10 +10,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable @@ -42,7 +38,7 @@ import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -63,7 +59,6 @@ private const val STAKING_REWARD_BLOCK_KEY = "StakingRewardBlock" private const val ACTIVE_STAKING_BLOCK_KEY = "ActiveStakingBlock" private const val STAKE_PRIMARY_BUTTON_KEY = "StakePrimaryButton" -@OptIn(ExperimentalMaterialApi::class) @Composable internal fun StakingInitialInfoContent( state: StakingStates.InitialInfoState, @@ -73,11 +68,9 @@ internal fun StakingInitialInfoContent( ) { if (state !is StakingStates.InitialInfoState.Data) return - val pullRefreshState = rememberPullRefreshState( - refreshing = state.pullToRefreshConfig.isRefreshing, - onRefresh = { state.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState()) }, - ) - Box(modifier = Modifier.pullRefresh(pullRefreshState)) { + TangemPullToRefreshContainer( + config = state.pullToRefreshConfig, + ) { LazyColumn( verticalArrangement = alignLastToBottom(), modifier = Modifier @@ -113,12 +106,6 @@ internal fun StakingInitialInfoContent( StakeButtonBlock(buttonState) } } - - PullRefreshIndicator( - modifier = Modifier.align(Alignment.TopCenter), - refreshing = state.pullToRefreshConfig.isRefreshing, - state = pullRefreshState, - ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 8e6b03d04c..9cc528d859 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -28,7 +28,6 @@ import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingAc import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingActionSelectorBottomSheet import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingInfoBottomSheet -import com.tangem.lib.crypto.BlockchainUtils.isSolana import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -170,8 +169,6 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M state = uiState.confirmationState, validatorState = uiState.validatorState, clickIntents = uiState.clickIntents, - type = uiState.actionType, - isSolana = isSolana(uiState.cryptoCurrencyBlockchainId), ) StakingStep.RestakeValidator, StakingStep.Validators, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 432a3256c3..1dc4ce0e48 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -232,17 +232,28 @@ internal class StakingViewModel @Inject constructor( } override fun onNextClick(balanceState: BalanceState?) { - if (value.currentStep == StakingStep.InitialInfo && balanceState == null) { - stateController.update( - SetConfirmationStateInitTransformer( - isEnter = true, - isExplicitExit = false, - balanceState = null, - cryptoCurrencyStatus = cryptoCurrencyStatus, - stakingApproval = stakingApproval, - stakingAllowance = stakingAllowance, - ), - ) + val isInitialInfoStep = value.currentStep == StakingStep.InitialInfo + val noBalanceState = balanceState == null + val noYieldBalanceData = cryptoCurrencyStatus.value.yieldBalance !is YieldBalance.Data + + when { + isInitialInfoStep && noBalanceState && yield.allValidatorsFull && noYieldBalanceData -> { + stakingEventFactory.createStakingValidatorsUnavailableAlert() + return + } + isInitialInfoStep && noBalanceState -> { + stateController.update( + SetConfirmationStateInitTransformer( + isEnter = true, + isExplicitExit = false, + balanceState = null, + cryptoCurrencyStatus = cryptoCurrencyStatus, + stakingApproval = stakingApproval, + stakingAllowance = stakingAllowance, + yieldArgs = yield.args, + ), + ) + } } stakingStateRouter.onNextClick() } @@ -711,7 +722,7 @@ internal class StakingViewModel @Inject constructor( feeValue = fee.orZero(), reduceAmountBy = reduceAmountBy, ) - StakingActionCommonType.Exit, + is StakingActionCommonType.Exit, is StakingActionCommonType.Pending, -> BigDecimal.ZERO } @@ -996,6 +1007,7 @@ internal class StakingViewModel @Inject constructor( pendingActions = pendingActions, pendingAction = pendingAction, stakingAllowance = stakingAllowance, + yieldArgs = yield.args, ), ValidatorSelectChangeTransformer( selectedValidator = validator, diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index f9d6ff0013..805cd27a59 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -27,6 +29,8 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.arrow.core) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) /** Domain */ implementation(projects.domain.tokens.models) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index ed7aa550c5..082f1b8ec9 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList @@ -26,6 +27,7 @@ internal class DefaultSwapTransactionRepository( ) : SwapTransactionRepository { private val converter = SavedSwapTransactionListConverter(excludedBlockchains) + private val userTokensResponseFactory = UserTokensResponseFactory() override suspend fun storeTransaction( userWalletId: UserWalletId, @@ -33,7 +35,13 @@ internal class DefaultSwapTransactionRepository( toCryptoCurrency: CryptoCurrency, transaction: SavedSwapTransactionModel, ) { - transaction.status?.let { storeTransactionState(transaction.txId, it) } + transaction.status?.let { + storeTransactionState( + txId = transaction.txId, + status = it, + refundTokenCurrency = null, + ) + } appPreferencesStore.editData { mutablePreferences -> val savedTransactions: List? = mutablePreferences.getObjectList( key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, @@ -154,14 +162,22 @@ internal class DefaultSwapTransactionRepository( } } - override suspend fun storeTransactionState(txId: String, status: ExchangeStatusModel) { + override suspend fun storeTransactionState( + txId: String, + status: ExchangeStatusModel, + refundTokenCurrency: CryptoCurrency?, + ) { appPreferencesStore.editData { mutablePreferences -> val savedMap = mutablePreferences.getObjectMap( key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, ) val updatesMap = savedMap.toMutableMap() - updatesMap[txId] = status + updatesMap[txId] = status.copy( + refundTokensResponse = refundTokenCurrency?.let { + userTokensResponseFactory.createResponseToken(refundTokenCurrency) + }, + ) mutablePreferences.setObjectMap( key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt index d3d6093b01..ed4ce2cf2e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt @@ -53,7 +53,15 @@ internal class SavedSwapTransactionListConverter( return SavedSwapTransactionListModel( transactions = value.transactions.map { tx -> - tx.copy(status = txStatuses[tx.txId]) + val status = txStatuses[tx.txId] + val refundCurrency = status?.refundTokensResponse?.let { id -> + responseCryptoCurrenciesFactory.createCurrency( + responseToken = id, + scanResponse = scanResponse, + ) + } + val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency) + tx.copy(status = statusWithRefundCurrency) }, userWalletId = value.userWalletId, fromCryptoCurrencyId = value.fromCryptoCurrencyId, diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 55f5e6eea3..ed5fe567e7 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -40,6 +42,7 @@ dependencies { /** Core modules */ implementation(projects.core.utils) implementation(projects.core.ui) + implementation(projects.core.datasource) /** Feature Apis */ implementation(projects.features.wallet.api) @@ -51,4 +54,5 @@ dependencies { implementation(deps.tangem.blockchain) implementation(deps.tangem.card.core) implementation(deps.moshi) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) } \ No newline at end of file diff --git a/features/swap/domain/models/build.gradle.kts b/features/swap/domain/models/build.gradle.kts index 431789f3d2..dd42b09ca2 100644 --- a/features/swap/domain/models/build.gradle.kts +++ b/features/swap/domain/models/build.gradle.kts @@ -1,3 +1,5 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) @@ -21,5 +23,7 @@ dependencies { /** Other Libraries **/ implementation(deps.kotlin.serialization) implementation(deps.arrow.core) + implementation(deps.moshi.kotlin) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt deleted file mode 100644 index 40cf980a73..0000000000 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.feature.swap.domain.models.domain - -import kotlinx.serialization.Serializable - -@Serializable -sealed class Currency { - abstract val id: String - abstract val name: String - abstract val symbol: String - abstract val networkId: String - abstract val logoUrl: String - - @Serializable - data class NativeToken( - override val id: String, - override val name: String, - override val symbol: String, - override val networkId: String, - override val logoUrl: String, - ) : Currency() - - @Serializable - data class NonNativeToken( - override val id: String, - override val name: String, - override val symbol: String, - override val networkId: String, - override val logoUrl: String, - val contractAddress: String, - val decimalCount: Int, - ) : Currency() -} - -fun Currency.isNonNative(): Boolean { - return this is Currency.NonNativeToken -} \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt deleted file mode 100644 index 41d7826e07..0000000000 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.feature.swap.domain.models.domain - -data class ExchangeQuote( - val toAmount: String, - val allowanceContract: String?, -) \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt index 95cc0f4d22..bd5e63b506 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt @@ -1,28 +1,85 @@ package com.tangem.feature.swap.domain.models.domain +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.tokens.model.CryptoCurrency + +@JsonClass(generateAdapter = true) data class ExchangeStatusModel( + @Json(name = "providerId") val providerId: String, + @Json(name = "status") val status: ExchangeStatus? = null, + @Json(name = "txId") val txId: String? = null, + @Json(name = "txExternalUrl") val txExternalUrl: String? = null, + @Json(name = "txExternalId") val txExternalId: String? = null, + @Json(name = "refundNetwork") val refundNetwork: String? = null, + @Json(name = "refundContractAddress") val refundContractAddress: String? = null, + @Json(name = "refundTokensResponse") + val refundTokensResponse: UserTokensResponse.Token? = null, + @Json(ignore = true) + val refundCurrency: CryptoCurrency? = null, ) +@JsonClass(generateAdapter = false) enum class ExchangeStatus { + @Json(name = "New") New, + + @Json(name = "Waiting") Waiting, + + @Json(name = "WaitingTxHash") WaitingTxHash, + + @Json(name = "Confirming") Confirming, + + @Json(name = "Verifying") Verifying, + + @Json(name = "Exchanging") Exchanging, + + @Json(name = "Failed") Failed, + + @Json(name = "Sending") Sending, + + @Json(name = "Finished") Finished, + + @Json(name = "Refunded") Refunded, + + @Json(name = "Cancelled") Cancelled, + + @Json(name = "TxFailed") TxFailed, + + @Json(name = "Unknown") Unknown, + + @Json(name = "Paused") Paused, + + ; + + val isTerminal: Boolean + get() = this == Refunded || + this == Finished || + this == Cancelled || + this == TxFailed || + this == Unknown + + val isAutoDisposable: Boolean + get() = this == Finished } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt index 2db24c66eb..357ec53b20 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt @@ -1,6 +1,12 @@ package com.tangem.feature.swap.domain.models.domain +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) data class SavedLastSwappedCryptoCurrency( + @Json(name = "userWalletId") val userWalletId: String, + @Json(name = "cryptoCurrencyId") val cryptoCurrencyId: String, ) \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt index d771aa4ecb..39fc4ef525 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt @@ -1,5 +1,7 @@ package com.tangem.feature.swap.domain.models.domain +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.tokens.model.CryptoCurrency import java.math.BigDecimal @@ -17,21 +19,35 @@ data class SavedSwapTransactionListModel( * IMPORTANT !!! * This is internal model for storing swap tx. It should not be used outside. */ +@JsonClass(generateAdapter = true) data class SavedSwapTransactionListModelInner( + @Json(name = "userWalletId") val userWalletId: String, + @Json(name = "fromCryptoCurrencyId") val fromCryptoCurrencyId: String, + @Json(name = "toCryptoCurrencyId") val toCryptoCurrencyId: String, + @Json(name = "fromTokensResponse") val fromTokensResponse: UserTokensResponse.Token? = null, + @Json(name = "toTokensResponse") val toTokensResponse: UserTokensResponse.Token? = null, + @Json(name = "transactions") val transactions: List, ) // TODO refactor to use separate models to store +@JsonClass(generateAdapter = true) data class SavedSwapTransactionModel( + @Json(name = "txId") val txId: String, + @Json(name = "timestamp") val timestamp: Long, + @Json(name = "fromCryptoAmount") val fromCryptoAmount: BigDecimal, + @Json(name = "toCryptoAmount") val toCryptoAmount: BigDecimal, + @Json(name = "provider") val provider: SwapProvider, + @Json(name = "status") val status: ExchangeStatusModel? = null, ) \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index 561858f9f1..abbcf27ea2 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -1,5 +1,7 @@ package com.tangem.feature.swap.domain.models.domain +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import com.tangem.domain.tokens.model.CryptoCurrencyStatus import java.math.BigDecimal @@ -31,22 +33,35 @@ data class CryptoCurrencySwapInfo( * Uses to store transaction data in datastore, when extends - should always add default value * to support backward compatibility */ +@JsonClass(generateAdapter = true) data class SwapProvider( + @Json(name = "providerId") val providerId: String, + @Json(name = "rateTypes") val rateTypes: List = emptyList(), + @Json(name = "name") val name: String, + @Json(name = "type") val type: ExchangeProviderType, + @Json(name = "imageLarge") val imageLarge: String, + @Json(name = "termsOfUse") val termsOfUse: String?, + @Json(name = "privacyPolicy") val privacyPolicy: String?, + @Json(name = "isRecommended") val isRecommended: Boolean = false, + @Json(name = "slippage") val slippage: BigDecimal?, ) +@JsonClass(generateAdapter = false) enum class ExchangeProviderType(val providerName: String) { - DEX("DEX"), - CEX("CEX"), - DEX_BRIDGE("DEX/Bridge"), + @Json(name = "DEX") DEX("DEX"), + + @Json(name = "CEX") CEX("CEX"), + + @Json(name = "DEX_BRIDGE") DEX_BRIDGE("DEX/Bridge"), } /** @@ -54,7 +69,9 @@ enum class ExchangeProviderType(val providerName: String) { * * Current implementation contains only float type, fixed will be supported later. */ +@JsonClass(generateAdapter = false) enum class RateType { - FLOAT, - FIXED, + @Json(name = "FLOAT") FLOAT, + + @Json(name = "FIXED") FIXED, } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt deleted file mode 100644 index ac5b7b2b05..0000000000 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.feature.swap.domain.models.domain - -/** - * Transaction model - * - * @property fromWalletAddress wallet from address (transactions will be sent from this address) - * @property toWalletAddress transactions will be sent to our(1inch) contract address - * @property data The encoded data to call the approve method on the swapped token contract - * @property value Native token value in WEI (for approve is always 0) - * @property gasPrice maximum amount of gas for a swap default: 11500000; max: 11500000 - * @property gas estimated amount of the gas limit, increase this value by 25% - */ -data class TransactionModel( - val fromWalletAddress: String, - val toWalletAddress: String, - val data: String, - val value: String, - val gasPrice: String, - val gas: String, -) \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt deleted file mode 100644 index 8d17d05808..0000000000 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.feature.swap.domain.models.ui - -import com.tangem.feature.swap.domain.models.domain.Currency - -data class TokensDataState( - val preselectTokens: PreselectTokens, - val foundTokensState: FoundTokensState, -) - -data class FoundTokensState( - val tokensInWallet: List, - val loadedTokens: List, -) - -data class PreselectTokens( - val fromToken: Currency, - val toToken: Currency, -) - -data class TokenWithBalance( - val token: Currency, - val tokenBalanceData: TokenBalanceData? = null, -) - -data class TokenBalanceData( - val amount: String?, - val amountEquivalent: String?, -) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 298af784a2..53e6b2335c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -980,6 +980,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( VanarChainTestnet, OdysseyChain, OdysseyChainTestnet, Bitrock, BitrockTestnet, + Sonic, SonicTestnet, + ApeChain, ApeChainTestnet, -> Fee.Common(feeAmount) // endregion } @@ -2132,14 +2134,17 @@ internal class SwapInteractorImpl @AssistedInject constructor( } private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { - val set = ids.toSet().getQuotesOrEmpty(false).filterIsInstance() + val set = ids.mapNotNull { it.rawCurrencyId } + .toSet() + .getQuotesOrEmpty(false) + .filterIsInstance() return ids .mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } } .toMap() } - private suspend fun Set.getQuotesOrEmpty(refresh: Boolean): Set { + private suspend fun Set.getQuotesOrEmpty(refresh: Boolean): Set { return try { quotesRepository.getQuotesSync(this, refresh) } catch (t: Throwable) { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt index f02a93f2ab..812499d47f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt @@ -30,7 +30,7 @@ interface SwapTransactionRepository { txId: String, ) - suspend fun storeTransactionState(txId: String, status: ExchangeStatusModel) + suspend fun storeTransactionState(txId: String, status: ExchangeStatusModel, refundTokenCurrency: CryptoCurrency?) suspend fun storeLastSwappedCryptoCurrencyId(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt index 7736b9a69d..bf1bfae85a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt @@ -35,7 +35,7 @@ internal object SwapNotificationUM { title = title, subtitle = subtitle ?: resourceReference(id = R.string.common_unknown_error), buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(R.string.common_ok), + text = resourceReference(R.string.common_retry), onClick = onConfirmClick, ), ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt index 401f98e20f..202f7100a6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt @@ -43,19 +43,15 @@ internal class SwapNotificationsFactory( } fun getGeneralErrorStateNotifications( - notifications: ImmutableList, message: TextReference?, onClick: () -> Unit, ): ImmutableList { - val updatedNotifications = notifications.toMutableList() - updatedNotifications.add( + return persistentListOf( SwapNotificationUM.Error.GenericError( subtitle = message, onConfirmClick = onClick, ), ) - - return updatedNotifications.toPersistentList() } fun getNotAvailableStateNotifications(fromCurrencyName: String): ImmutableList { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt new file mode 100644 index 0000000000..1be63f7e6f --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/preview/SwapSuccessStatePreview.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.swap.preview + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType +import com.tangem.feature.swap.models.SwapSuccessStateHolder + +internal data object SwapSuccessStatePreview { + val state = SwapSuccessStateHolder( + timestamp = 0L, + txUrl = "https://www.google.com/#q=nam", + fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), + providerName = TextReference.Str("1inch"), + providerType = TextReference.Str(ExchangeProviderType.DEX.providerName), + showStatusButton = false, + providerIcon = "", + fromTokenAmount = TextReference.Str("1 000 DAI"), + toTokenAmount = TextReference.Str("1 000 MATIC"), + fromTokenFiatAmount = TextReference.Str("1 000 $"), + toTokenFiatAmount = TextReference.Str("1 000 $"), + fromTokenIconState = CurrencyIconState.Loading, + toTokenIconState = CurrencyIconState.Loading, + rate = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), + onExploreButtonClick = {}, + onStatusButtonClick = {}, + ) +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 7e1c08759d..d743a4ebda 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -5,9 +5,9 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 2cbb63b407..0a7f59c547 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -911,7 +911,6 @@ internal class StateBuilder( fun addNotification(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder { return uiState.copy( notifications = notificationsFactory.getGeneralErrorStateNotifications( - notifications = uiState.notifications, message = message, onClick = onClick, ), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index deecc7282f..5744e49278 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -8,7 +8,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -21,6 +21,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.common.ui.notifications.NotificationUM @@ -256,14 +257,20 @@ private fun getAnnotatedStringForLegalsWithClick( } } -@OptIn(ExperimentalMaterialApi::class) @Composable private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { Card( - elevation = TangemTheme.dimens.elevation3, + elevation = CardDefaults.cardElevation( + defaultElevation = 2.dp, + pressedElevation = 2.dp, + ), shape = CircleShape, - backgroundColor = TangemTheme.colors.background.action, - contentColor = TangemTheme.colors.text.primary1, + colors = CardDefaults.cardColors( + containerColor = TangemTheme.colors.background.action, + disabledContainerColor = TangemTheme.colors.background.action, + contentColor = TangemTheme.colors.text.primary1, + disabledContentColor = TangemTheme.colors.text.primary1, + ), modifier = modifier.size(TangemTheme.dimens.size48), onClick = state.onChangeCardsClicked, enabled = state.changeCardsButtonState == ChangeCardsButtonState.ENABLED, @@ -308,13 +315,6 @@ private fun SwapNotifications(notifications: List) { ) { notifications.forEach { notification -> when (notification) { - is SwapNotificationUM.Error.GenericError -> { - RefreshableWarningCard( - title = notification.title.resolveReference(), - description = notification.config.subtitle.resolveReference(), - onClick = notification.onConfirmClick, - ) - } is SwapNotificationUM.Error.ApprovalInProgressWarning, is SwapNotificationUM.Error.TransactionInProgressWarning, -> { @@ -341,7 +341,9 @@ private fun SwapNotifications(notifications: List) { is NotificationUM.Info, is NotificationUM.Warning, -> null - is NotificationUM.Error -> TangemTheme.colors.icon.warning + is SwapNotificationUM.Error.GenericError, + is NotificationUM.Error, + -> TangemTheme.colors.icon.warning }, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index c1cafdc166..7e4d9649bf 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -4,7 +4,7 @@ import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material.Scaffold +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -24,15 +24,15 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat -import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType import com.tangem.feature.swap.models.SwapSuccessStateHolder import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.preview.SwapSuccessStatePreview @Composable fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) { Scaffold( modifier = Modifier.systemBarsPadding(), - backgroundColor = TangemTheme.colors.background.secondary, + containerColor = TangemTheme.colors.background.secondary, content = { padding -> SwapSuccessScreenContent(padding = padding, state = state) }, @@ -162,32 +162,12 @@ private fun SwapSuccessScreenButtons( } // region preview - -private val state = SwapSuccessStateHolder( - timestamp = 0L, - txUrl = "https://www.google.com/#q=nam", - fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), - providerName = TextReference.Str("1inch"), - providerType = TextReference.Str(ExchangeProviderType.DEX.providerName), - showStatusButton = false, - providerIcon = "", - fromTokenAmount = TextReference.Str("1 000 DAI"), - toTokenAmount = TextReference.Str("1 000 MATIC"), - fromTokenFiatAmount = TextReference.Str("1 000 $"), - toTokenFiatAmount = TextReference.Str("1 000 $"), - fromTokenIconState = CurrencyIconState.Loading, - toTokenIconState = CurrencyIconState.Loading, - rate = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), - onExploreButtonClick = {}, - onStatusButtonClick = {}, -) - @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun Preview_Success() { TangemThemePreview { - SwapSuccessScreen(state) {} + SwapSuccessScreen(SwapSuccessStatePreview.state) {} } } // endregion preview \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 86afbab205..5c994aba93 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { /** Domain modules */ implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) + implementation(projects.domain.feedback) /** Other libraries */ implementation(deps.arrow.core) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 5a9fbd3fe3..23e833f935 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -135,7 +135,7 @@ internal class TesterActivity : ComposeActivity() { composable(route = TesterScreen.BLOCKCHAIN_PROVIDERS.name) { val viewModel = hiltViewModel().apply { - setupNavigation(innerTesterRouter) + setupNavigation(innerTesterRouter, appFinisher) } val state by viewModel.state.collectAsStateWithLifecycle() diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt index c814bf750b..9307de43a7 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt @@ -1,21 +1,26 @@ package com.tangem.feature.tester.presentation.actions +import androidx.compose.runtime.Immutable import com.tangem.domain.apptheme.model.AppThemeMode +import java.io.File internal data class TesterActionsContentState( - val hideAllCurrenciesConfig: HideAllCurrenciesConfig, - val toggleAppThemeConfig: ToggleAppThemeConfig, + val hideAllCurrenciesUM: HideAllCurrenciesUM, + val toggleAppThemeUM: ToggleAppThemeUM, + val shareLogsUM: ShareLogsUM, val onBackClick: () -> Unit, - val onApplyChangesClick: () -> Unit, -) +) { + sealed class HideAllCurrenciesUM { + data class Clickable(val onClick: () -> Unit) : HideAllCurrenciesUM() -internal sealed class HideAllCurrenciesConfig { - data class Clickable(val onClick: () -> Unit) : HideAllCurrenciesConfig() + data object Progress : HideAllCurrenciesUM() + } - data object Progress : HideAllCurrenciesConfig() -} + data class ToggleAppThemeUM( + val currentAppTheme: AppThemeMode, + val onClick: () -> Unit, + ) -internal data class ToggleAppThemeConfig( - val currentAppTheme: AppThemeMode, - val onClick: () -> Unit, -) \ No newline at end of file + @Immutable + data class ShareLogsUM(val file: File?) +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt index adadf6886f..f19b69a825 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt @@ -1,5 +1,7 @@ package com.tangem.feature.tester.presentation.actions +import android.app.Activity +import android.content.Intent import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -8,14 +10,24 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.app.ShareCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.findActivity import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM +import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleAppThemeUM +import timber.log.Timber +import java.io.File @OptIn(ExperimentalFoundationApi::class) @Composable @@ -24,6 +36,7 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod modifier = modifier .fillMaxSize() .background(TangemTheme.colors.background.secondary), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { stickyHeader { AppBarWithBackButton( @@ -31,42 +44,75 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod onBackClick = state.onBackClick, ) } + item { - val onClick = remember(state.hideAllCurrenciesConfig) { - { (state.hideAllCurrenciesConfig as? HideAllCurrenciesConfig.Clickable)?.onClick?.invoke() ?: Unit } + val onClick = remember(state.hideAllCurrenciesUM) { + { (state.hideAllCurrenciesUM as? HideAllCurrenciesUM.Clickable)?.onClick?.invoke() ?: Unit } } TesterActionItem( name = stringResourceSafe(R.string.hide_all_currencies), - progress = state.hideAllCurrenciesConfig is HideAllCurrenciesConfig.Progress, + progress = state.hideAllCurrenciesUM is HideAllCurrenciesUM.Progress, onClick = onClick, ) } + item { - val config = state.toggleAppThemeConfig + val config = state.toggleAppThemeUM TesterActionItem( name = stringResourceSafe(id = R.string.toggle_app_theme, config.currentAppTheme.name), onClick = config.onClick, ) } + + item { + val activity = LocalContext.current.findActivity() + + TesterActionItem( + name = stringResourceSafe(id = R.string.share_logs), + onClick = { activity.shareFile(file = state.shareLogsUM.file) }, + enabled = state.shareLogsUM.file != null, + ) + } } } @Composable -private fun TesterActionItem( - name: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - progress: Boolean = false, -) { - Box(modifier = modifier.padding(all = TangemTheme.dimens.spacing16)) { - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = name, - onClick = onClick, - showProgress = progress, +private fun TesterActionItem(name: String, onClick: () -> Unit, progress: Boolean = false, enabled: Boolean = true) { + PrimaryButton( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + text = name, + onClick = onClick, + showProgress = progress, + enabled = enabled, + ) +} + +private fun Activity.shareFile(file: File?) { + val originalIntent = createEmailShareIntent(activity = this, file = file) + + try { + val chooserIntent = Intent.createChooser(originalIntent, "Share logs...") + + ContextCompat.startActivity(this, chooserIntent, null) + } catch (ex: Exception) { + Timber.e("Failed to share file: $ex") + } +} + +private fun createEmailShareIntent(activity: Activity, file: File?): Intent { + val builder = ShareCompat.IntentBuilder(activity) + .setType("text/plain") + + file?.let { + builder.setStream( + FileProvider.getUriForFile(activity, "${activity.packageName}.provider", it), ) } + + return builder.intent } // region Preview @@ -78,10 +124,10 @@ private fun TesterActionsScreenSample(modifier: Modifier = Modifier) { ) { TesterActionsScreen( state = TesterActionsContentState( - hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable {}, - toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT) {}, + hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable {}, + toggleAppThemeUM = ToggleAppThemeUM(AppThemeMode.DEFAULT) {}, + shareLogsUM = TesterActionsContentState.ShareLogsUM(file = null), onBackClick = {}, - onApplyChangesClick = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt index 514edb448d..1c351420c7 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt @@ -9,6 +9,9 @@ import arrow.core.getOrElse import com.tangem.domain.apptheme.ChangeAppThemeModeUseCase import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM +import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleAppThemeUM import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.lib.crypto.UserWalletManager import dagger.hilt.android.lifecycle.HiltViewModel @@ -24,6 +27,7 @@ internal class TesterActionsViewModel @Inject constructor( private val userWalletManager: UserWalletManager, private val changeAppThemeModeUseCase: ChangeAppThemeModeUseCase, private val getAppThemeModeUseCase: GetAppThemeModeUseCase, + private val feedbackRepository: FeedbackRepository, ) : ViewModel() { var uiState: TesterActionsContentState by mutableStateOf(initialState) @@ -31,10 +35,13 @@ internal class TesterActionsViewModel @Inject constructor( private val initialState: TesterActionsContentState get() = TesterActionsContentState( - hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable(this::hideAllCurrencies), - toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT, this::toggleAppTheme), + hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable(this::hideAllCurrencies), + toggleAppThemeUM = ToggleAppThemeUM( + currentAppTheme = AppThemeMode.DEFAULT, + onClick = this::toggleAppTheme, + ), + shareLogsUM = TesterActionsContentState.ShareLogsUM(file = feedbackRepository.getLogFile()), onBackClick = { /* no-op */ }, - onApplyChangesClick = { /* no-op */ }, ) init { @@ -47,17 +54,17 @@ internal class TesterActionsViewModel @Inject constructor( private fun hideAllCurrencies() = viewModelScope.launch { uiState = uiState.copy( - hideAllCurrenciesConfig = HideAllCurrenciesConfig.Progress, + hideAllCurrenciesUM = HideAllCurrenciesUM.Progress, ) userWalletManager.hideAllTokens() uiState = uiState.copy( - hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable(this@TesterActionsViewModel::hideAllCurrencies), + hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable(this@TesterActionsViewModel::hideAllCurrencies), ) } private fun toggleAppTheme() = viewModelScope.launch { - val currentAppThemeMode = uiState.toggleAppThemeConfig.currentAppTheme + val currentAppThemeMode = uiState.toggleAppThemeUM.currentAppTheme val newAppThemeMode = when (currentAppThemeMode) { AppThemeMode.FORCE_DARK -> AppThemeMode.FORCE_LIGHT AppThemeMode.FORCE_LIGHT -> AppThemeMode.FOLLOW_SYSTEM @@ -89,13 +96,13 @@ internal class TesterActionsViewModel @Inject constructor( Timber.d( """ Current app theme mode updated - |- Previous app theme mode: ${uiState.toggleAppThemeConfig.currentAppTheme} + |- Previous app theme mode: ${uiState.toggleAppThemeUM.currentAppTheme} |- New app theme mode: $maybeAppThemeMode """.trimIndent(), ) uiState = uiState.copy( - toggleAppThemeConfig = uiState.toggleAppThemeConfig.copy( + toggleAppThemeUM = uiState.toggleAppThemeUM.copy( currentAppTheme = maybeAppThemeMode.getOrElse { error -> Timber.e( """ diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/entity/BlockchainProvidersUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/entity/BlockchainProvidersUM.kt index f63f49d696..53571d98f9 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/entity/BlockchainProvidersUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/entity/BlockchainProvidersUM.kt @@ -10,6 +10,7 @@ internal data class BlockchainProvidersUM( val topBar: TopBarWithRefreshUM, val searchBar: SearchBarUM, val blockchainProviders: ImmutableList, + val onRestartAppClick: () -> Unit, ) { @Immutable diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/ui/BlockchainProvidersScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/ui/BlockchainProvidersScreen.kt index fab6c7afac..908033f11b 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/ui/BlockchainProvidersScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/ui/BlockchainProvidersScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.util.fastForEachIndexed import com.tangem.blockchain.common.network.providers.ProviderType import com.tangem.core.ui.components.AdditionalTextInputDialogUM import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TextInputDialog import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.entity.SearchBarUM @@ -59,7 +60,7 @@ internal fun BlockchainProvidersScreen(state: BlockchainProvidersUM) { .fillMaxSize() .background(TangemTheme.colors.background.primary), ) { - LazyColumn { + LazyColumn(contentPadding = PaddingValues(bottom = 64.dp)) { stickyHeader { Header(topBar = state.topBar, searchBar = state.searchBar) } @@ -85,6 +86,15 @@ internal fun BlockchainProvidersScreen(state: BlockchainProvidersUM) { itemContent = { BlockchainProvidersItem(it) }, ) } + + PrimaryButton( + text = stringResourceSafe(id = R.string.restart_app), + onClick = state.onRestartAppClick, + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing16), + ) } } @@ -427,6 +437,7 @@ private fun Preview_BlockchainProvidersScreen() { ), ), ), + onRestartAppClick = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/viewmodel/BlockchainProvidersViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/viewmodel/BlockchainProvidersViewModel.kt index 97cf7d9d36..97c4512a84 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/viewmodel/BlockchainProvidersViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/providers/viewmodel/BlockchainProvidersViewModel.kt @@ -7,6 +7,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.network.providers.ProviderType import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.MutableBlockchainProvidersTypesManager +import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.feature.tester.impl.R @@ -46,6 +47,7 @@ internal class BlockchainProvidersViewModel @Inject constructor( onActiveChange = {}, ), blockchainProviders = persistentListOf(), + onRestartAppClick = {}, ), ) @@ -57,10 +59,11 @@ internal class BlockchainProvidersViewModel @Inject constructor( subscribeOnBlockchainProviderTypesUpdates() } - fun setupNavigation(router: InnerTesterRouter) { + fun setupNavigation(router: InnerTesterRouter, appFinisher: AppFinisher) { _state.update { state -> state.copy( topBar = state.topBar.copy(onBackClick = router::back), + onRestartAppClick = appFinisher::restart, ) } } @@ -116,7 +119,7 @@ internal class BlockchainProvidersViewModel @Inject constructor( private fun onRefreshClick() { viewModelScope.launch { - manager.update() + manager.recoverInitialState() } } diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 77b8a9c63d..12d59827a6 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -13,4 +13,5 @@ Excluded blockchains Filter by name or symbol Blockchain providers + Share logs diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 83ab6680a0..db1345ff87 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index 8e3ed14b1c..365f441978 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -38,7 +38,7 @@ internal class TokenDetailsNotificationsAnalyticsSender( currency = cryptoCurrency, ) is TokenDetailsNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( - programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, + programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action source = AnalyticsParam.ScreensSources.Token, ) is TokenDetailsNotification.KaspaIncompleteTransactionWarning -> TokenDetailsAnalyticsEvent.Notice.Reveal( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index ff23815212..594aba1cca 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import kotlinx.collections.immutable.ImmutableList diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt index ce5a9f3d49..7df750dad1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt @@ -1,6 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.components import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.tokendetails.impl.R @@ -133,5 +134,22 @@ internal data class TokenDetailsDialogConfig( onClick = onConfirmClick, ) } + + data class ConfirmExpressStatusHideDialogConfig( + val onConfirmClick: () -> Unit, + val onCancelClick: () -> Unit, + ) : DialogContentConfig() { + override val title: TextReference = resourceReference(R.string.express_status_hide_dialog_title) + override val message: TextReference = resourceReference(R.string.express_status_hide_dialog_text) + override val confirmButtonConfig: ButtonConfig = ButtonConfig( + text = resourceReference(R.string.common_hide), + onClick = onConfirmClick, + ) + + override val cancelButtonConfig: ButtonConfig = ButtonConfig( + text = resourceReference(R.string.common_cancel), + onClick = onCancelClick, + ) + } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExchangeUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExchangeUM.kt index 4420f4be4a..ac23c537eb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExchangeUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExchangeUM.kt @@ -15,7 +15,6 @@ internal data class ExchangeUM( val statuses: ImmutableList, val notification: ExchangeStatusNotifications? = null, val showProviderLink: Boolean, - val isRefundTerminalStatus: Boolean, val fromCryptoCurrency: CryptoCurrency, val toCryptoCurrency: CryptoCurrency, ) : ExpressTransactionStateUM \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index 3b8d00da73..ec6b983685 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -94,6 +94,7 @@ internal class TokenDetailsOnrampTransactionStateConverter( clickIntents.onGoToProviderClick(it) }, onClick = { clickIntents.onExpressTransactionClick(value.txId) }, + onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus, ), providerName = value.providerName, providerImageUrl = value.providerImageUrl, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 6e56b18248..3a5d25730d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -6,7 +6,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.card.NetworkHasDerivationUseCase import com.tangem.domain.staking.GetStakingIntegrationIdUseCase diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index d93b495c65..e88c1cc061 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -340,6 +340,22 @@ internal class TokenDetailsStateFactory( return balanceSelectStateConverter.convert(buttonConfig) } + fun getStateWithConfirmHideExpressStatus(): TokenDetailsState { + return currentStateProvider().copy( + dialogConfig = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmExpressStatusHideDialogConfig( + onConfirmClick = { + clickIntents.onDisposeExpressStatus() + clickIntents.onDismissDialog() + }, + onCancelClick = clickIntents::onDismissDialog, + ), + ), + ) + } + private fun TokenDetailsAppBarMenuConfig.updateMenu( cardTypesResolver: CardTypesResolver, hasDerivations: Boolean, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 18fe759bac..ba05cd229d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -80,17 +80,20 @@ internal class TokenDetailsSwapTransactionsStateConverter( fromFiatAmount = quote.fiatRate.multiply(fromAmount) } } - val notifications = - getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null) + val statusModel = transaction.status + val notifications = getNotification( + status = statusModel?.status, + txUrl = statusModel?.txExternalUrl, + refundToken = statusModel?.refundCurrency, + ) val showProviderLink = getShowProviderLink(notifications, transaction.status) result.add( ExchangeUM( provider = transaction.provider, - statuses = getStatuses(transaction.status?.status), + statuses = getStatuses(statusModel?.status), notification = notifications, - activeStatus = transaction.status?.status, + activeStatus = statusModel?.status, showProviderLink = showProviderLink, - isRefundTerminalStatus = true, fromCryptoCurrency = fromCryptoCurrency, toCryptoCurrency = toCryptoCurrency, info = createStateInfo( @@ -107,25 +110,19 @@ internal class TokenDetailsSwapTransactionsStateConverter( return result.toPersistentList() } - fun updateTxStatus( - tx: ExchangeUM, - statusModel: ExchangeStatusModel?, - refundToken: CryptoCurrency?, - isRefundTerminalStatus: Boolean, - ): ExchangeUM { + fun updateTxStatus(tx: ExchangeUM, statusModel: ExchangeStatusModel?): ExchangeUM { if (statusModel == null || tx.activeStatus == statusModel.status) { Timber.e("UpdateTxStatus isn't required. Current status isn't changed") return tx } val hasFailed = statusModel.status == ExchangeStatus.Failed - val notifications = getNotification(statusModel.status, statusModel.txExternalUrl, refundToken) + val notifications = getNotification(statusModel.status, statusModel.txExternalUrl, statusModel.refundCurrency) val showProviderLink = getShowProviderLink(notifications, statusModel) return tx.copy( activeStatus = statusModel.status, notification = notifications, statuses = getStatuses(statusModel.status, hasFailed), showProviderLink = showProviderLink, - isRefundTerminalStatus = isRefundTerminalStatus, info = tx.info.copy(txExternalUrl = statusModel.txExternalUrl), ) } @@ -164,7 +161,8 @@ internal class TokenDetailsSwapTransactionsStateConverter( }, iconState = getIconState(transaction.status?.status), status = getStatusState(), - notification = null, // fixme [REDACTED_JIRA] + notification = null, // fixme [REDACTED_JIRA], + onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index 25d87bd106..3bf62b35de 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -79,13 +79,13 @@ internal class ExchangeStatusFactory @AssistedInject constructor( } } - suspend fun removeTransactionOnBottomSheetClosed(isForceTerminal: Boolean = false) { + suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean = false) { val state = currentStateProvider() val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return val selectedTx = bottomSheetConfig.value as? ExchangeUM ?: return - val shouldTerminate = selectedTx.activeStatus.isTerminal(selectedTx.isRefundTerminalStatus) || isForceTerminal - if (shouldTerminate) { + val shouldDispose = selectedTx.activeStatus?.isAutoDisposable == true || isForceDispose + if (shouldDispose) { swapTransactionRepository.removeTransaction( userWalletId = userWalletId, fromCryptoCurrency = selectedTx.fromCryptoCurrency, @@ -96,20 +96,14 @@ internal class ExchangeStatusFactory @AssistedInject constructor( } suspend fun updateSwapTxStatus(swapTx: ExchangeUM): ExchangeUM { - return if (swapTx.activeStatus.isTerminal(swapTx.isRefundTerminalStatus)) { + return if (swapTx.activeStatus?.isTerminal == true) { swapTx } else { val statusModel = getExchangeStatus(swapTx.info.txId, swapTx.provider) - val isRefundTerminalStatus = statusModel?.refundNetwork == null && - statusModel?.refundContractAddress == null && - swapTx.provider.type != ExchangeProviderType.DEX_BRIDGE - val addedRefundToken = addRefundCurrencyIfNeeded(statusModel, swapTx.provider.type) swapTransactionsStateConverter.updateTxStatus( tx = swapTx, statusModel = statusModel, - refundToken = addedRefundToken, - isRefundTerminalStatus = isRefundTerminalStatus, ) } } @@ -120,8 +114,11 @@ internal class ExchangeStatusFactory @AssistedInject constructor( ifLeft = { null }, ifRight = { statusModel -> sendStatusUpdateAnalytics(statusModel, provider) - swapTransactionRepository.storeTransactionState(txId, statusModel) - statusModel + + val refundTokenCurrency = addRefundCurrencyIfNeeded(statusModel, provider.type) + + swapTransactionRepository.storeTransactionState(txId, statusModel, refundTokenCurrency) + statusModel.copy(refundCurrency = refundTokenCurrency) }, ) } @@ -174,15 +171,6 @@ internal class ExchangeStatusFactory @AssistedInject constructor( ) } - private fun ExchangeStatus?.isTerminal(isRefundTerminal: Boolean): Boolean { - val needTerminalRefund = this == ExchangeStatus.Refunded && isRefundTerminal - return needTerminalRefund || - this == ExchangeStatus.Finished || - this == ExchangeStatus.Cancelled || - this == ExchangeStatus.TxFailed || - this == ExchangeStatus.Unknown - } - private fun toAnalyticStatus(status: ExchangeStatus?): ExpressAnalyticsStatus? { return when (status) { ExchangeStatus.New, @@ -205,7 +193,8 @@ internal class ExchangeStatusFactory @AssistedInject constructor( private suspend fun Set.getQuotesOrEmpty(refresh: Boolean): Set { return try { - quotesRepository.getQuotesSync(this, refresh) + val rawIds = mapNotNull { it.rawCurrencyId }.toSet() + quotesRepository.getQuotesSync(rawIds, refresh) } catch (t: Throwable) { emptySet() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt index 3c1633c710..5f1c18949f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt @@ -157,11 +157,13 @@ internal class ExpressStatusFactory @AssistedInject constructor( suspend fun removeTransactionOnBottomSheetClosed( expressState: ExpressTransactionStateUM, - isForceTerminal: Boolean = false, + isForceDispose: Boolean = false, ) { when (expressState) { - is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceTerminal) - is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed() + is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceDispose) + is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed( + isForceDispose, + ) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index 7ca14864da..79328454af 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -68,12 +68,12 @@ internal class OnrampStatusFactory @AssistedInject constructor( } } - suspend fun removeTransactionOnBottomSheetClosed() { + suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean) { val state = currentStateProvider() val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.OnrampUM ?: return - if (selectedTx.activeStatus.isTerminal) { + if (selectedTx.activeStatus.isAutoDisposable || isForceDispose) { onrampRemoveTransactionUseCase(externalTxId = selectedTx.info.txExternalId) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 0591214019..f60d8ad078 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -5,14 +5,9 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview @@ -28,10 +23,9 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.notifications.OkxPromoNotification import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData @@ -48,7 +42,6 @@ import com.tangem.features.markets.token.block.TokenMarketBlockComponent // TODO: Split to blocks [REDACTED_JIRA] @Suppress("LongMethod") -@OptIn(ExperimentalMaterialApi::class) @Composable internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockComponent: TokenMarketBlockComponent?) { BackHandler(onBack = state.topAppBarConfig.onBackClick) @@ -59,11 +52,6 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> - val pullRefreshState = rememberPullRefreshState( - refreshing = state.pullToRefreshConfig.isRefreshing, - onRefresh = { state.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState()) }, - ) - val txHistoryItems = if (state.txHistoryState is TxHistoryState.Content) { state.txHistoryState.contentItems.collectAsLazyPagingItems() } else { @@ -75,10 +63,9 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon .padding(top = betweenItemsPadding) .padding(horizontal = horizontalPadding) - Box( - modifier = Modifier - .padding(scaffoldPaddings) - .pullRefresh(pullRefreshState), + TangemPullToRefreshContainer( + config = state.pullToRefreshConfig, + modifier = Modifier.padding(scaffoldPaddings), ) { LazyColumn( modifier = Modifier.fillMaxSize(), @@ -104,21 +91,14 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon key = { it::class.java }, contentType = { it.config::class.java }, itemContent = { - if (it is TokenDetailsNotification.SwapPromo) { - OkxPromoNotification( - config = it.config, - modifier = itemModifier.animateItem(), - ) - } else { - Notification( - modifier = itemModifier.animateItem(), - config = it.config, - iconTint = when (it) { - is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent - else -> null - }, - ) - } + Notification( + modifier = itemModifier.animateItem(), + config = it.config, + iconTint = when (it) { + is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent + else -> null + }, + ) }, ) @@ -169,12 +149,6 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon txHistoryItems = txHistoryItems, ) } - - PullRefreshIndicator( - modifier = Modifier.align(Alignment.TopCenter), - refreshing = state.pullToRefreshConfig.isRefreshing, - state = pullRefreshState, - ) } TokenDetailsDialogs(state = state) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBlock.kt index 7e0091568c..33fcbc4cf8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBlock.kt @@ -8,8 +8,8 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Icon +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt index 3c969fa8d2..f547861942 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import com.tangem.common.ui.expressStatus.ExpressEstimate +import com.tangem.common.ui.expressStatus.ExpressHideButton import com.tangem.common.ui.expressStatus.ExpressProvider import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH10 @@ -70,6 +71,11 @@ internal fun ExchangeStatusBottomSheetContent(state: ExchangeUM) { if (state.notification != null) { Notification(state = state.notification, activeStatus = state.activeStatus) } + ExpressHideButton( + isTerminal = state.activeStatus?.isTerminal == true, + isAutoDisposable = state.activeStatus?.isAutoDisposable == true, + onClick = state.info.onDisposeExpressStatus, + ) SpacerH24() } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index e4439079b5..8806affc45 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -72,4 +72,8 @@ interface TokenDetailsClickIntents { fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) fun onOpenUrlClick(url: String) + + fun onConfirmDisposeExpressStatus() + + fun onDisposeExpressStatus() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index d3ffbeb33b..744218305e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -36,7 +36,6 @@ import com.tangem.domain.card.NetworkHasDerivationUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.promo.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase @@ -74,6 +73,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory +import com.tangem.domain.promo.ShouldShowSwapPromoTokenUseCase import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider @@ -784,16 +784,6 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) { - val bottomSheetState = internalUiState.value.bottomSheetConfig?.content - if (bottomSheetState is ExpressStatusBottomSheetConfig) { - viewModelScope.launch { - expressStatusFactory.removeTransactionOnBottomSheetClosed( - expressState = bottomSheetState.value, - isForceTerminal = true, - ) - } - } - internalUiState.value = stateFactory.getStateWithClosedBottomSheet() router.openTokenDetails(userWalletId, cryptoCurrency) } @@ -807,7 +797,7 @@ internal class TokenDetailsViewModel @Inject constructor( analyticsEventsHandler.send( TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Token, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, + programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed, ), ) @@ -820,7 +810,7 @@ internal class TokenDetailsViewModel @Inject constructor( analyticsEventsHandler.send( TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Token, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, + programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked, ), ) @@ -950,6 +940,23 @@ internal class TokenDetailsViewModel @Inject constructor( internalUiState.value = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config) } + override fun onConfirmDisposeExpressStatus() { + internalUiState.value = stateFactory.getStateWithConfirmHideExpressStatus() + } + + override fun onDisposeExpressStatus() { + val bottomSheetState = internalUiState.value.bottomSheetConfig?.content + if (bottomSheetState is ExpressStatusBottomSheetConfig) { + viewModelScope.launch { + expressStatusFactory.removeTransactionOnBottomSheetClosed( + expressState = bottomSheetState.value, + isForceDispose = true, + ) + } + } + internalUiState.value = stateFactory.getStateWithClosedBottomSheet() + } + private fun handleUnavailabilityReason( unavailabilityReason: ScenarioUnavailabilityReason, tokenSymbol: String, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt index 902d26f1ef..d079febfc3 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetWalletNamesUseCase import com.tangem.domain.wallets.usecase.RenameWalletUseCase import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.feature.walletsettings.entity.RenameWalletUM @@ -26,17 +27,20 @@ import timber.log.Timber internal class DefaultRenameWalletComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: RenameWalletComponent.Params, + getWalletNamesUseCase: GetWalletNamesUseCase, private val renameWalletUseCase: RenameWalletUseCase, ) : RenameWalletComponent, AppComponentContext by context { - private val currentWalletName = params.currentName + private val initialName = params.currentName + private val walletNames by lazy(getWalletNamesUseCase::invoke) private val stateFlow: MutableStateFlow = MutableStateFlow( value = RenameWalletUM( walletNameValue = TextFieldValue(text = params.currentName), - updateValue = ::updateValue, + onValueChange = ::updateValue, isConfirmEnabled = false, onConfirm = { renameWallet(params.userWalletId) }, + error = null, ), ) @@ -48,24 +52,31 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( override fun Dialog() { val model by stateFlow.collectAsStateWithLifecycle() - RenameWalletDialog( - model = model, - onDismiss = ::dismiss, - ) + RenameWalletDialog(model = model, onDismiss = ::dismiss) } private fun updateValue(value: TextFieldValue) { stateFlow.update { + val isNameAlreadyExists = initialName != value.text && walletNames.contains(value.text) + it.copy( walletNameValue = value, - isConfirmEnabled = value.text.isNotBlank() && value.text != currentWalletName, + isConfirmEnabled = value.text.isNotBlank() && + value.text != initialName && + !isNameAlreadyExists, + error = if (isNameAlreadyExists) { + resourceReference( + id = R.string.user_wallet_list_rename_popup_error_already_exists, + formatArgs = wrappedList(value.text), + ) + } else { + null + }, ) } } private fun renameWallet(userWalletId: UserWalletId) = componentScope.launch { - stateFlow.update { it.copy(isConfirmEnabled = false) } - val newName = stateFlow.value.walletNameValue val maybeError = renameWalletUseCase(userWalletId, newName.text).leftOrNull() @@ -76,14 +87,14 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( is UpdateWalletError.DataError -> resourceReference(id = R.string.common_unknown_error) is UpdateWalletError.NameAlreadyExists -> resourceReference( id = R.string.user_wallet_list_rename_popup_error_already_exists, - formatArgs = wrappedList(newName), + formatArgs = wrappedList(newName.text), ) } messageSender.send(message = SnackbarMessage(message)) + } else { + dismiss() } - - dismiss() } @AssistedFactory diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt index fbcb3d6389..12834f5b0d 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt @@ -11,8 +11,9 @@ internal class PreviewRenameWalletComponent : RenameWalletComponent { private val previewState = RenameWalletUM( walletNameValue = TextFieldValue(text = "My Wallet"), isConfirmEnabled = false, - updateValue = {}, + onValueChange = {}, onConfirm = {}, + error = null, ) override fun dismiss() {} diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt index 730cc1ee60..23549c9809 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt @@ -2,11 +2,13 @@ package com.tangem.feature.walletsettings.entity import androidx.compose.runtime.Immutable import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.core.ui.extensions.TextReference @Immutable internal data class RenameWalletUM( val walletNameValue: TextFieldValue, - val updateValue: (value: TextFieldValue) -> Unit, + val onValueChange: (value: TextFieldValue) -> Unit, val isConfirmEnabled: Boolean, val onConfirm: () -> Unit, + val error: TextReference?, ) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt index 0e6313e933..0d32c07b9e 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.AdditionalTextInputDialogUM import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.components.TextInputDialog +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.walletsettings.component.preview.PreviewRenameWalletComponent @@ -31,9 +32,11 @@ internal fun RenameWalletDialog(model: RenameWalletUM, onDismiss: () -> Unit) { onClick = onDismiss, ), onDismissDialog = onDismiss, - onValueChange = model.updateValue, + onValueChange = model.onValueChange, textFieldParams = AdditionalTextInputDialogUM( label = stringResourceSafe(id = R.string.user_wallet_list_rename_popup_placeholder), + isError = model.error != null, + caption = model.error?.resolveReference(), ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 614ce24018..12300eadb2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -7,7 +7,7 @@ import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index cbe0c57c5f..6313ea4653 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -4,6 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent.ProgramName import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState @@ -50,7 +51,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.NoteMigration -> MainScreen.NotePromo is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, + programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action ) is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] is WalletNotification.Informational.NoAccount, @@ -58,10 +59,6 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Warning.SomeNetworksUnreachable, is WalletNotification.Warning.NetworksUnreachable, -> null - is WalletNotification.RingPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( - source = AnalyticsParam.ScreensSources.Main, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.Ring, - ) is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index b852180742..d863a3dea9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -4,9 +4,6 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.promo.PromoRepository -import com.tangem.domain.promo.ShouldShowRingPromoUseCase -import com.tangem.domain.promo.models.PromoBanner import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -22,7 +19,6 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flow import javax.inject.Inject import kotlin.collections.count @@ -32,8 +28,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val tokenListStore: MultiWalletTokenListStore, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, - private val shouldShowRingPromoUseCase: ShouldShowRingPromoUseCase, - private val promoRepository: PromoRepository, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, private val backupValidator: BackupValidator, private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, @@ -43,25 +37,13 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver - val promoFlow = flow { emit(promoRepository.getRingPromoBanner()) } return combine( - tokenListStore.getOrThrow(userWallet.walletId), - isReadyToShowRateAppUseCase(), - isNeedToBackupUseCase(userWallet.walletId), - shouldShowRingPromoUseCase(userWalletId = userWallet.walletId), - promoFlow, - seedPhraseNotificationUseCase(userWalletId = userWallet.walletId), - ) { - val maybeTokenList = it[0] as Lce - val isReadyToShowRating = it[1] as Boolean - val isNeedToBackup = it[2] as Boolean - val shouldShowPromo = it[3] as Boolean - val promoBanner = it[4] as? PromoBanner - val seedPhraseIssueStatus = it[5] as Boolean - + flow = tokenListStore.getOrThrow(userWallet.walletId), + flow2 = isReadyToShowRateAppUseCase(), + flow3 = isNeedToBackupUseCase(userWallet.walletId), + flow4 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId), + ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, seedPhraseIssueStatus -> buildList { - addRingPromoNotification(shouldShowPromo, promoBanner, clickIntents) - addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents) @@ -79,19 +61,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( } } - private fun MutableList.addRingPromoNotification( - shouldShowPromo: Boolean, - promoBanner: PromoBanner?, - clickIntents: WalletClickIntents, - ) { - promoBanner ?: return - val promoNotification = WalletNotification.RingPromo(onCloseClick = clickIntents::onCloseRingPromoClick) - addIf( - element = promoNotification, - condition = shouldShowPromo && promoBanner.isActive, - ) - } - private fun MutableList.addCriticalNotifications( userWallet: UserWallet, seedPhraseIssueStatus: Boolean, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt index 7686df2258..32450b5201 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -30,12 +30,12 @@ internal class OnrampStatusFactory @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) { - suspend fun removeTransactionOnBottomSheetClosed() { + suspend fun removeTransactionOnBottomSheetClosed(forceDispose: Boolean) { val state = stateHolder.getSelectedWallet() val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.OnrampUM ?: return - if (selectedTx.activeStatus.isTerminal) { + if (selectedTx.activeStatus.isAutoDisposable || forceDispose) { onrampRemoveTransactionUseCase(externalTxId = selectedTx.info.txExternalId) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt index 5c8ab22a90..7d8fe9b661 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt @@ -16,6 +16,8 @@ internal sealed interface WalletAlertState { open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) open val isWarningConfirmButton: Boolean = false abstract val onConfirmClick: (() -> Unit)? + open val cancelButtonText: TextReference? = null + open val onCancelClick: (() -> Unit)? = null } @Immutable @@ -36,6 +38,7 @@ internal sealed interface WalletAlertState { override val onConfirmClick: (() -> Unit)?, ) : Basic() + // TODO: reuse RenameWalletComponent [REDACTED_JIRA] data class RenameWalletAlert( override val text: String, override val onConfirmClick: (String) -> Unit, @@ -107,4 +110,14 @@ internal sealed interface WalletAlertState { override val onConfirmClick: (() -> Unit)? = null } + + data class ConfirmExpressStatusHide( + override val onConfirmClick: (() -> Unit), + override val onCancelClick: (() -> Unit), + ) : Basic() { + override val title: TextReference = resourceReference(R.string.express_status_hide_dialog_title) + override val message: TextReference = resourceReference(R.string.express_status_hide_dialog_text) + override val confirmButtonText: TextReference = resourceReference(R.string.common_hide) + override val cancelButtonText: TextReference = resourceReference(R.string.common_cancel) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 2d80995272..dfcfda9298 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -206,16 +206,6 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ) - data class RingPromo(val onCloseClick: () -> Unit) : WalletNotification( - config = NotificationConfig( - title = resourceReference(id = R.string.ring_promo_title), - subtitle = resourceReference(id = R.string.ring_promo_text), - iconResId = R.drawable.img_card_with_ring, - backgroundResId = R.drawable.img_ring_promo_background, - onCloseClick = onCloseClick, - ), - ) - data class NoteMigration(val onClick: () -> Unit) : WalletNotification( config = NotificationConfig( title = resourceReference(R.string.wallet_promo_banner_title), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 4f86e6685c..7eed57588c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -5,7 +5,7 @@ import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedTxHistoryStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedWalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt index 4211ef693c..fcb666ff4d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model.holder import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 68e485e6eb..9977790553 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.* import kotlinx.collections.immutable.PersistentList diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt index 606d0ed064..a5a0ee99b1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt @@ -62,9 +62,7 @@ internal class SingleWalletOnrampTransactionConverter( value.timestamp.toTimeFormat(), ), ), - toAmount = stringReference( - value.toAmount.format { crypto(currency) }, - ), + toAmount = stringReference(value.toAmount.format { crypto(currency) }), toFiatAmount = stringReference( status.fiatRate?.multiply(value.toAmount).format { fiat( @@ -94,6 +92,7 @@ internal class SingleWalletOnrampTransactionConverter( analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) clickIntents.onGoToProviderClick(it) }, + onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus, onClick = { val analyticEvent = TokenOnrampAnalyticsEvent.OnrampStatusOpened( tokenSymbol = currency.symbol, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletEventSender.kt index 34859d54d9..710e54acde 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletEventSender.kt @@ -21,7 +21,7 @@ internal class WalletEventSender @Inject constructor( stateHolder.update(transformer = SendEventTransformer(event = event, onConsume = ::onConsume)) } - private fun onConsume() { + fun onConsume() { stateHolder.update { it.copy(event = consumedEvent()) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 45b1b98237..601894c802 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.utils import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 6a8f8eb2bc..828bee05c9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -15,9 +15,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -61,7 +58,7 @@ import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme @@ -285,7 +282,7 @@ private fun WalletContent( } @Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod") -@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable private inline fun BaseScaffoldWithMarkets( state: WalletScreenState, @@ -387,13 +384,6 @@ private inline fun BaseScaffoldWithMarkets( } }, content = { paddingValues -> - val pullRefreshState = rememberPullRefreshState( - refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = { - selectedWallet.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState()) - }, - ) - Box { MarketsHint( modifier = Modifier @@ -405,16 +395,8 @@ private inline fun BaseScaffoldWithMarkets( Column { WalletTopBar(config = state.topBarConfig) - Box( - modifier = Modifier.pullRefresh(pullRefreshState), - ) { + TangemPullToRefreshContainer(config = selectedWallet.pullToRefreshConfig) { content(paddingValues) - - WalletPullToRefreshIndicator( - isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 81ea0beb94..3c12b92a98 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -5,8 +5,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.notifications.OkxPromoNotification -import com.tangem.core.ui.components.notifications.RingPromoNotification import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -28,16 +26,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList { - OkxPromoNotification( - config = it.config, - modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), - ) - } - is WalletNotification.RingPromo -> { - RingPromoNotification( - config = it.config, - modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), - ) + // Use it on new promo action } is WalletNotification.NoteMigration -> { NoteMigrationNotification( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPullToRefreshIndicator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPullToRefreshIndicator.kt deleted file mode 100644 index 5c26f03441..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPullToRefreshIndicator.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.common - -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.PullRefreshState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -/** - * "Pull to refresh" indicator - * - * @param isRefreshing indicator is currently refreshing or not - * @param state indicator state - * @param modifier modifier - * -[REDACTED_AUTHOR] - */ -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun WalletPullToRefreshIndicator( - isRefreshing: Boolean, - state: PullRefreshState, - modifier: Modifier = Modifier, -) { - PullRefreshIndicator(refreshing = isRefreshing, state = state, modifier = modifier) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt index 441f6e8c56..4511cf32ef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt @@ -107,32 +107,33 @@ private fun TransactionBlock(transaction: VisaTxDetailsBottomSheetConfig.Transac ) } +@Suppress("LongMethod") @Composable private fun BlockchainRequestBlock(request: VisaTxDetailsBottomSheetConfig.Request, modifier: Modifier = Modifier) { BlockContent( modifier = modifier, title = stringReference(value = "Blockchain request"), description = { - if (request.onExploreClick == null) return - - Row( - modifier = Modifier.clickable(onClick = request.onExploreClick), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4), - ) { - Icon( - painter = painterResource(id = R.drawable.ic_compass_24), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size18), - tint = TangemTheme.colors.icon.informative, - ) - Text( - text = "Explore", - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption1, - ) + if (request.onExploreClick != null) { + Row( + modifier = Modifier.clickable(onClick = request.onExploreClick), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_compass_24), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size18), + tint = TangemTheme.colors.icon.informative, + ) + Text( + text = "Explore", + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + ) + } + SpacerW12() } - SpacerW12() }, content = { BlockItem( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt index 1826afb74d..a04cb17d49 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt @@ -2,11 +2,11 @@ package com.tangem.feature.wallet.presentation.wallet.ui.utils import android.app.Activity import android.content.Context -import android.content.ContextWrapper import com.google.android.gms.tasks.Task import com.google.android.play.core.review.ReviewInfo import com.google.android.play.core.review.ReviewManager import com.google.android.play.core.review.ReviewManagerFactory +import com.tangem.core.ui.utils.findActivity import timber.log.Timber internal object ReviewManagerRequester { @@ -44,13 +44,4 @@ internal object ReviewManagerRequester { Timber.e(task.exception) } } - - private fun Context.findActivity(): Activity { - var context = this - while (context is ContextWrapper) { - if (context is Activity) return context - context = context.baseContext - } - error("Permissions should be called in the context of an Activity") - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 0b20b60c75..3d00a7564b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -15,12 +15,14 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCurrencyActionsConverter +import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.flow.collectLatest @@ -54,6 +56,10 @@ internal interface WalletContentClickIntents { fun onGoToProviderClick(externalTxId: String) fun onExpressTransactionClick(txId: String) + + fun onConfirmDisposeExpressStatus() + + fun onDisposeExpressStatus() } @Suppress("LongParameterList") @@ -70,6 +76,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase, private val dispatchers: CoroutineDispatcherProvider, private val reduxStateHolder: ReduxStateHolder, + private val walletEventSender: WalletEventSender, ) : BaseWalletClickIntents(), WalletContentClickIntents { override fun onBackClick() = router.popBackStack() @@ -185,7 +192,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( val userWalletId = stateHolder.getSelectedWalletId() if (stateHolder.getSelectedWallet().bottomSheetConfig?.content is ExpressStatusBottomSheetConfig) { viewModelScope.launch(dispatchers.main) { - onrampStatusFactory.removeTransactionOnBottomSheetClosed() + onrampStatusFactory.removeTransactionOnBottomSheetClosed(forceDispose = false) } } stateHolder.update(CloseBottomSheetTransformer(userWalletId)) @@ -211,4 +218,28 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( ) } } + + override fun onConfirmDisposeExpressStatus() { + walletEventSender.send( + WalletEvent.ShowAlert( + WalletAlertState.ConfirmExpressStatusHide( + onConfirmClick = { + walletEventSender.onConsume() + onDisposeExpressStatus() + }, + onCancelClick = walletEventSender::onConsume, + ), + ), + ) + } + + override fun onDisposeExpressStatus() { + val userWalletId = stateHolder.getSelectedWalletId() + if (stateHolder.getSelectedWallet().bottomSheetConfig?.content is ExpressStatusBottomSheetConfig) { + viewModelScope.launch(dispatchers.main) { + onrampStatusFactory.removeTransactionOnBottomSheetClosed(forceDispose = true) + } + } + stateHolder.update(CloseBottomSheetTransformer(userWalletId)) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index ee807b127a..732ee16502 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -11,7 +11,6 @@ import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.promo.ShouldShowRingPromoUseCase import com.tangem.domain.promo.ShouldShowSwapPromoWalletUseCase import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder @@ -66,8 +65,6 @@ internal interface WalletWarningsClickIntents { fun onCloseSwapPromoClick() - fun onCloseRingPromoClick() - fun onSupportClick() fun onNoteMigrationButtonClick(url: String) @@ -94,7 +91,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val reduxStateHolder: ReduxStateHolder, private val dispatchers: CoroutineDispatcherProvider, private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, - private val shouldShowRingPromoUseCase: ShouldShowRingPromoUseCase, private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, @@ -249,7 +245,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( analyticsEventHandler.send( TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( source = AnalyticsParam.ScreensSources.Main, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, + programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed, ), ) @@ -258,20 +254,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } - override fun onCloseRingPromoClick() { - analyticsEventHandler.send( - TokenSwapPromoAnalyticsEvent.PromotionBannerClicked( - source = AnalyticsParam.ScreensSources.Main, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.Ring, - action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed, - ), - ) - - viewModelScope.launch { - shouldShowRingPromoUseCase.neverToShow() - } - } - override fun onSupportClick() { val scanResponse = getSelectedUserWallet()?.scanResponse ?: return val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index aa063deab4..e27db46828 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -58,11 +58,11 @@ kotlin-immutable-collections = "0.3.5" kotsonGsonExt = "2.5.0" lottie = "3.4.0" lottie-compose = "6.6.0" -moshi = "1.12.0" +moshi = "1.15.1" okhttp = "4.9.3" rekotlin = "1.0.4" -retrofit = "2.8.1" -retrofitMoshiConverter = "2.6.0" +retrofit = "2.11.0" +retrofitMoshiConverter = "2.9.0" spongycastleCryptoCore = "1.58.0.0" timber = "4.7.1" viewBindingDelegate = "1.5.9" @@ -89,9 +89,9 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.20-937" +tangemBlockchainSdk = "develop-940" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.20-431" +tangemCardSdk = "develop-432" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ @@ -241,11 +241,13 @@ material = { module = "com.google.android.material:material", version.ref = "goo moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" } +moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" } spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "spongycastleCryptoCore" } reKotlin = { module = "org.rekotlin:rekotlin", version.ref = "rekotlin" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofit-response-type-keeper = { module = "com.squareup.retrofit2:response-type-keeper", version.ref = "retrofit" } retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofitMoshiConverter" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } viewBindingDelegate = { module = "com.github.kirich1409:viewbindingpropertydelegate-noreflection", version.ref = "viewBindingDelegate" } diff --git a/libs/blockchain-sdk/build.gradle.kts b/libs/blockchain-sdk/build.gradle.kts index 96dcda0905..12234d749f 100644 --- a/libs/blockchain-sdk/build.gradle.kts +++ b/libs/blockchain-sdk/build.gradle.kts @@ -1,7 +1,10 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) id("configuration") } @@ -22,7 +25,7 @@ dependencies { // endregion // region DI libraries - implementation(deps.hilt.core) + implementation(deps.hilt.android) kapt(deps.hilt.kapt) // endregion @@ -31,6 +34,8 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.timber) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion // region Firebase libraries diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt index e1a287d634..b988b3fce1 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt @@ -1,39 +1,43 @@ package com.tangem.blockchainsdk.converters import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.network.providers.ProviderType import com.tangem.blockchainsdk.BlockchainProvidersResponse import com.tangem.blockchainsdk.providers.BlockchainProviderTypes -import com.tangem.blockchainsdk.utils.createPrivateProviderType import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.datasource.local.config.providers.models.ProviderModel -import com.tangem.utils.converter.Converter +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.utils.converter.TwoWayConverter import timber.log.Timber /** - * Converts [BlockchainProvidersResponse] to [BlockchainProviderTypes] + * Converts [BlockchainProvidersResponse] to [BlockchainProviderTypes] and vice versa * [REDACTED_AUTHOR] */ internal object BlockchainProviderTypesConverter : - Converter { + TwoWayConverter { override fun convert(value: BlockchainProvidersResponse): BlockchainProviderTypes { return value.mapNotNull { (networkId, blockchainProviders) -> val blockchain = Blockchain.fromNetworkId(networkId) ?: return@mapNotNull null - val providerTypes = blockchainProviders.mapNotNull { provider -> - when (provider) { - is ProviderModel.Public -> ProviderType.Public(url = provider.url) - is ProviderModel.Private -> createPrivateProviderType(name = provider.name) - ProviderModel.UnsupportedType -> { - Timber.e("$blockchain provider type is not supported") - null - } - } + val providerTypes = ProviderTypeConverter.convertList(input = blockchainProviders) + + providerTypes.forEach { + if (it == null) Timber.e("$blockchain provider type is not supported") } - blockchain to providerTypes + blockchain to providerTypes.filterNotNull() + } + .toMap() + } + + override fun convertBack(value: BlockchainProviderTypes): BlockchainProvidersResponse { + return value.mapNotNull { (blockchain, providerTypes) -> + val networkId = blockchain.toNetworkId() + + val blockchainProviders = ProviderTypeConverter.convertListBack(input = providerTypes) + + networkId to blockchainProviders } .toMap() } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/ProviderTypeConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/ProviderTypeConverter.kt new file mode 100644 index 0000000000..9958db8a21 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/ProviderTypeConverter.kt @@ -0,0 +1,35 @@ +package com.tangem.blockchainsdk.converters + +import com.tangem.blockchain.common.network.providers.ProviderType +import com.tangem.blockchainsdk.providers.ProviderTypeIdMapping +import com.tangem.datasource.local.config.providers.models.ProviderModel +import com.tangem.utils.converter.TwoWayConverter + +/** + * Converts [ProviderModel] to [ProviderType] and vice versa + * +[REDACTED_AUTHOR] + */ +internal object ProviderTypeConverter : TwoWayConverter { + + override fun convert(value: ProviderModel): ProviderType? { + return when (value) { + is ProviderModel.Public -> ProviderType.Public(url = value.url) + is ProviderModel.Private -> ProviderTypeIdMapping.entries.firstOrNull { it.id == value.name }?.providerType + ProviderModel.UnsupportedType -> null + } + } + + override fun convertBack(value: ProviderType?): ProviderModel { + return when (value) { + null -> ProviderModel.UnsupportedType + is ProviderType.Public -> ProviderModel.Public(url = value.url) + else -> { + val id = ProviderTypeIdMapping.entries.firstOrNull { it.providerType == value }?.id + ?: return ProviderModel.UnsupportedType + + ProviderModel.Private(id) + } + } + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index ec39268431..0e46908b4f 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -1,17 +1,26 @@ package com.tangem.blockchainsdk.di +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi import com.tangem.blockchain.common.logging.BlockchainSDKLogger +import com.tangem.blockchainsdk.BlockchainProvidersResponse import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.DefaultBlockchainSDKFactory import com.tangem.blockchainsdk.WalletManagerFactoryCreator import com.tangem.blockchainsdk.accountcreator.DefaultAccountCreator import com.tangem.blockchainsdk.datastorage.DefaultBlockchainDataStorage import com.tangem.blockchainsdk.featuretoggles.DefaultBlockchainSDKFeatureToggles +import com.tangem.blockchainsdk.providers.BlockchainProviderTypesStore import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.DevBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.ProdBlockchainProvidersTypesManager +import com.tangem.blockchainsdk.providers.dev.BlockchainProvidersResponseSerializer import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.libs.blockchain_sdk.BuildConfig @@ -19,7 +28,10 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -45,16 +57,35 @@ internal object BlockchainSDKFactoryModule { @Provides @Singleton fun provideBlockchainProvidersTypesManager( - devBlockchainProvidersTypesManager: DevBlockchainProvidersTypesManager, prodBlockchainProvidersTypesManager: ProdBlockchainProvidersTypesManager, + blockchainProviderTypesStore: BlockchainProviderTypesStore, + changedBlockchainProvidersStore: DataStore, ): BlockchainProvidersTypesManager { - return if (BuildConfig.DEBUG) { - devBlockchainProvidersTypesManager + return if (BuildConfig.TESTER_MENU_ENABLED) { + DevBlockchainProvidersTypesManager( + prodBlockchainProvidersTypesManager = prodBlockchainProvidersTypesManager, + blockchainProviderTypesStore = blockchainProviderTypesStore, + changedBlockchainProvidersStore = changedBlockchainProvidersStore, + ) } else { prodBlockchainProvidersTypesManager } } + @Provides + @Singleton + fun provideChangedBlockchainProvidersResponseDataStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): DataStore { + return DataStoreFactory.create( + serializer = BlockchainProvidersResponseSerializer(moshi), + produceFile = { context.dataStoreFile("changed_providers") }, + scope = CoroutineScope(dispatchers.io + SupervisorJob()), + ) + } + @Provides @Singleton fun provideWalletManagerFactoryCreator( diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt index 8afaacc12d..ec57ec97ac 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt @@ -3,7 +3,6 @@ package com.tangem.blockchainsdk.providers import androidx.core.util.PatternsCompat import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchainsdk.BlockchainProvidersResponse -import com.tangem.blockchainsdk.utils.createPrivateProviderType import com.tangem.datasource.local.config.providers.models.ProviderModel import timber.log.Timber @@ -48,11 +47,11 @@ internal object BlockchainProvidersResponseMerger { return result.guaranteeUrlsEndWithSlash() } - private fun List.filterUnsupportedProviders() = filter { - val isSupportedType = it !is ProviderModel.UnsupportedType + private fun List.filterUnsupportedProviders() = filter { model -> + val isSupportedType = model !is ProviderModel.UnsupportedType - val isSupportedPrivateType = if (it is ProviderModel.Private) { - createPrivateProviderType(it.name) != null + val isSupportedPrivateType = if (model is ProviderModel.Private) { + ProviderTypeIdMapping.entries.any { it.id == model.name } } else { true } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersTypesManager.kt index af43d092c8..6b2d23a725 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersTypesManager.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersTypesManager.kt @@ -1,11 +1,11 @@ package com.tangem.blockchainsdk.providers -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow /** Blockchain providers types manager */ interface BlockchainProvidersTypesManager { - fun get(): StateFlow + fun get(): Flow suspend fun update() } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt index 479c90f1d2..8af47fd124 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt @@ -1,45 +1,68 @@ package com.tangem.blockchainsdk.providers +import androidx.datastore.core.DataStore import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.network.providers.ProviderType -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.properties.Delegates.notNull +import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import timber.log.Timber /** * Implementation of [BlockchainProvidersTypesManager] in DEV environment * * @property prodBlockchainProvidersTypesManager prod manager * @property blockchainProviderTypesStore blockchain provider types store + * @property changedBlockchainProvidersStore changed blockchain providers store * [REDACTED_AUTHOR] */ -@Singleton -internal class DevBlockchainProvidersTypesManager @Inject constructor( +internal class DevBlockchainProvidersTypesManager( private val prodBlockchainProvidersTypesManager: ProdBlockchainProvidersTypesManager, private val blockchainProviderTypesStore: BlockchainProviderTypesStore, -) : BlockchainProvidersTypesManager by prodBlockchainProvidersTypesManager, - MutableBlockchainProvidersTypesManager { + private val changedBlockchainProvidersStore: DataStore, +) : MutableBlockchainProvidersTypesManager { - private var primaryResponse: BlockchainProviderTypes by notNull() + override fun get(): Flow = changedBlockchainProvidersStore.data + .map(BlockchainProviderTypesConverter::convert) override suspend fun update() { prodBlockchainProvidersTypesManager.update() - primaryResponse = blockchainProviderTypesStore.get().value + val initial = blockchainProviderTypesStore.get().value + + val changed = changedBlockchainProvidersStore.data.firstOrNull().orEmpty() + + if (changed.isEmpty()) { + Timber.i("Initialize ChangedBlockchainProvidersStore") + changedBlockchainProvidersStore.updateData { + BlockchainProviderTypesConverter.convertBack(initial) + } + } + } + + override suspend fun recoverInitialState() { + val initial = blockchainProviderTypesStore.get().value + + changedBlockchainProvidersStore.updateData { + BlockchainProviderTypesConverter.convertBack(initial) + } } override suspend fun update(blockchain: Blockchain, providers: List) { - val currentTypes = blockchainProviderTypesStore.get().value + changedBlockchainProvidersStore.updateData { + val providerModels = BlockchainProviderTypesConverter.convertBack(value = mapOf(blockchain to providers)) - val updatedTypes = currentTypes + (blockchain to providers) - - blockchainProviderTypesStore.store(value = updatedTypes) + it + providerModels + } } override suspend fun isMatchWithMerged(): Boolean { - val current = blockchainProviderTypesStore.get().value + val initial = blockchainProviderTypesStore.get().value + val changed = changedBlockchainProvidersStore.data.firstOrNull().orEmpty() - return current == primaryResponse + return initial == BlockchainProviderTypesConverter.convert(changed) } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/MutableBlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/MutableBlockchainProvidersTypesManager.kt index 148a2dfc23..fd460c4ad9 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/MutableBlockchainProvidersTypesManager.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/MutableBlockchainProvidersTypesManager.kt @@ -13,6 +13,9 @@ interface MutableBlockchainProvidersTypesManager : BlockchainProvidersTypesManag /** Update [providers] for [blockchain] */ suspend fun update(blockchain: Blockchain, providers: List) + /** Recover initial state */ + suspend fun recoverInitialState() + /** Checks that current [BlockchainProviderTypes] was changed */ suspend fun isMatchWithMerged(): Boolean } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt index 3a62906d7b..c535a0f61a 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt @@ -1,7 +1,7 @@ package com.tangem.blockchainsdk.providers import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -20,7 +20,7 @@ internal class ProdBlockchainProvidersTypesManager @Inject constructor( private val blockchainProviderTypesStore: BlockchainProviderTypesStore, ) : BlockchainProvidersTypesManager { - override fun get(): StateFlow = blockchainProviderTypesStore.get() + override fun get(): Flow = blockchainProviderTypesStore.get() override suspend fun update() { val response = blockchainProvidersResponseLoader.load() diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProviderTypeIdMapping.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProviderTypeIdMapping.kt new file mode 100644 index 0000000000..6917699851 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProviderTypeIdMapping.kt @@ -0,0 +1,27 @@ +package com.tangem.blockchainsdk.providers + +import com.tangem.blockchain.common.network.providers.ProviderType + +/** Mapping of [providerType] with [id] */ +internal enum class ProviderTypeIdMapping(val id: String, val providerType: ProviderType) { + NowNodes(id = "nownodes", providerType = ProviderType.NowNodes), + GetBlock(id = "getblock", providerType = ProviderType.GetBlock), + QuickNode(id = "quicknode", providerType = ProviderType.QuickNode), + BitcoinBlockchair(id = "blockchair", providerType = ProviderType.BitcoinLike.Blockchair), + BitcoinBlockcypher(id = "blockcypher", providerType = ProviderType.BitcoinLike.Blockcypher), + CardanoAdalite(id = "adalite", providerType = ProviderType.Cardano.Adalite), + CardanoRosetta(id = "tangemRosetta", providerType = ProviderType.Cardano.Rosetta), + ChiaFireAcademy(id = "fireAcademy", providerType = ProviderType.Chia.FireAcademy), + ChiaTangem(id = "tangemChia", providerType = ProviderType.Chia.Tangem), + ChiaTangemNew(id = "tangemChia3", providerType = ProviderType.Chia.TangemNew), + EthereumInfura(id = "infura", providerType = ProviderType.EthereumLike.Infura), + HederaArkhia(id = "arkhiaHedera", providerType = ProviderType.Hedera.Arkhia), + KaspaSecondary(id = "kaspa", providerType = ProviderType.Kaspa.SecondaryAPI), + SolanaOfficial(id = "solana", providerType = ProviderType.Solana.Official), + TonCentral(id = "ton", providerType = ProviderType.Ton.TonCentral), + TronGrid(id = "tron", providerType = ProviderType.Tron.TronGrid), + BittensorDwellir(id = "dwellirBittensor", providerType = ProviderType.Bittensor.Dwellir), + BittensorOnfinality(id = "onfinalityBittensor", providerType = ProviderType.Bittensor.Onfinality), + KoinosPro(id = "koinospro", providerType = ProviderType.Koinos.KoinosPro), + ; +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/dev/BlockchainProvidersResponseSerializer.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/dev/BlockchainProvidersResponseSerializer.kt new file mode 100644 index 0000000000..be75a477db --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/dev/BlockchainProvidersResponseSerializer.kt @@ -0,0 +1,34 @@ +package com.tangem.blockchainsdk.providers.dev + +import androidx.datastore.core.Serializer +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.datasource.local.config.providers.models.ProviderModel +import java.io.InputStream +import java.io.OutputStream + +/** DataStore serializer for [BlockchainProvidersResponse]. Implemented by [Moshi]. */ +internal class BlockchainProvidersResponseSerializer(moshi: Moshi) : Serializer { + + private val adapter by lazy { + val providersType = Types.newParameterizedType(List::class.java, ProviderModel::class.java) + val type = Types.newParameterizedType(Map::class.java, String::class.java, providersType) + + moshi.adapter(type) + } + + override val defaultValue: BlockchainProvidersResponse = emptyMap() + + override suspend fun readFrom(input: InputStream): BlockchainProvidersResponse { + return input.bufferedReader().use { reader -> + adapter.fromJson(reader.readText()) ?: defaultValue + } + } + + override suspend fun writeTo(t: BlockchainProvidersResponse, output: OutputStream) { + output.bufferedWriter().use { writer -> + writer.write(adapter.toJson(t)) + } + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index a0d9c16d6f..e3e4cb47b3 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -152,6 +152,10 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "dione/test" -> Blockchain.OdysseyChainTestnet "bitrock" -> Blockchain.Bitrock "bitrock/test" -> Blockchain.BitrockTestnet + "sonic" -> Blockchain.Sonic + "sonic/test" -> Blockchain.SonicTestnet + "apechain" -> Blockchain.ApeChain + "apechain/test" -> Blockchain.ApeChainTestnet else -> null } } @@ -301,6 +305,10 @@ fun Blockchain.toNetworkId(): String { Blockchain.OdysseyChainTestnet -> "dione/test" Blockchain.Bitrock -> "bitrock" Blockchain.BitrockTestnet -> "bitrock/test" + Blockchain.Sonic -> "sonic" + Blockchain.SonicTestnet -> "sonic/test" + Blockchain.ApeChain -> "apechain" + Blockchain.ApeChainTestnet -> "apechain/test" } } @@ -398,6 +406,8 @@ fun Blockchain.toCoinId(): String { Blockchain.Clore -> "clore-ai" Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet -> "dione" Blockchain.Bitrock, Blockchain.BitrockTestnet -> "bitrock" + Blockchain.Sonic, Blockchain.SonicTestnet -> "sonic-3" + Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apecoin" } } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/ProviderTypeExt.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/ProviderTypeExt.kt deleted file mode 100644 index 3b32c69ae9..0000000000 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/ProviderTypeExt.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.blockchainsdk.utils - -import com.tangem.blockchain.common.network.providers.ProviderType -import timber.log.Timber - -/** Create private provider by [name] or return null */ -@Suppress("CyclomaticComplexMethod") -fun createPrivateProviderType(name: String): ProviderType? { - return when (name) { - "blockchair" -> ProviderType.BitcoinLike.Blockchair - "blockcypher" -> ProviderType.BitcoinLike.Blockcypher - "adalite" -> ProviderType.Cardano.Adalite - "tangemRosetta" -> ProviderType.Cardano.Rosetta - "fireAcademy" -> ProviderType.Chia.FireAcademy - "tangemChia" -> ProviderType.Chia.Tangem - "tangemChia3" -> ProviderType.Chia.TangemNew - "infura" -> ProviderType.EthereumLike.Infura - "getblock" -> ProviderType.GetBlock - "arkhiaHedera" -> ProviderType.Hedera.Arkhia - "kaspa" -> ProviderType.Kaspa.SecondaryAPI - "nownodes" -> ProviderType.NowNodes - "quicknode" -> ProviderType.QuickNode - "solana" -> ProviderType.Solana.Official - "ton" -> ProviderType.Ton.TonCentral - "tron" -> ProviderType.Tron.TronGrid - "dwellirBittensor" -> ProviderType.Bittensor.Dwellir - "onfinalityBittensor" -> ProviderType.Bittensor.Onfinality - "koinospro" -> ProviderType.Koinos.KoinosPro - else -> { - Timber.e("Private provider with name $name is not supported") - null - } - } -} \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index d86b31b3d0..754f3ea551 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -17,11 +17,13 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaActivationInput import com.tangem.domain.visa.model.VisaAuthChallenge -import com.tangem.domain.visa.model.VisaCardActivationResponse +import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter +import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.sdk.api.visa.VisaCardActivationResponse @Suppress("TooManyFunctions") interface TangemSdkManager { @@ -153,5 +155,7 @@ interface TangemSdkManager { activationInput: VisaActivationInput, ): CompletionResult + suspend fun visaCustomerWalletApprove(visaDataForApprove: VisaDataForApprove): CompletionResult + // endregion } \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/visa/VisaCardActivationResponse.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/visa/VisaCardActivationResponse.kt new file mode 100644 index 0000000000..50da653b5e --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/visa/VisaCardActivationResponse.kt @@ -0,0 +1,11 @@ +package com.tangem.sdk.api.visa + +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.visa.model.SignedActivationOrder +import com.tangem.domain.visa.model.VisaRootOTP + +data class VisaCardActivationResponse( + val signedActivationOrder: SignedActivationOrder, + val rootOTP: VisaRootOTP, + val newCardDTO: CardDTO, +) \ No newline at end of file diff --git a/libs/visa/build.gradle.kts b/libs/visa/build.gradle.kts index 8ef254a4e4..eb6f922306 100644 --- a/libs/visa/build.gradle.kts +++ b/libs/visa/build.gradle.kts @@ -1,6 +1,9 @@ +import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants + plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) id("configuration") } @@ -21,6 +24,8 @@ dependencies { implementation(deps.okHttp.prettyLogging) implementation(deps.retrofit) implementation(deps.retrofit.moshi) + kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) + kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) /** Libs - Other */ implementation(deps.web3j.core) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApi.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApi.kt index b05b3096a2..106ced7a60 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApi.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApi.kt @@ -3,12 +3,14 @@ package com.tangem.lib.visa.api import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.lib.visa.model.VisaTxHistoryResponse import retrofit2.http.GET +import retrofit2.http.Header import retrofit2.http.Query interface VisaApi { @GET("transaction") suspend fun getTxHistory( + @Header("Authorization") authorizationHeader: String, @Query("card_public_key") cardPublicKey: String, @Query("limit") limit: Int, @Query("offset") offset: Int, diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaTxHistoryResponse.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaTxHistoryResponse.kt index dcec060570..bf722ca7bf 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaTxHistoryResponse.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaTxHistoryResponse.kt @@ -1,9 +1,11 @@ package com.tangem.lib.visa.model import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import org.joda.time.DateTime import java.math.BigDecimal +@JsonClass(generateAdapter = true) data class VisaTxHistoryResponse( @Json(name = "card_wallet_address") val cardWalletAddress: String, @@ -11,6 +13,7 @@ data class VisaTxHistoryResponse( val transactions: List, ) { + @JsonClass(generateAdapter = true) data class Transaction( @Json(name = "auth_code") val authCode: String?, @@ -52,6 +55,7 @@ data class VisaTxHistoryResponse( val transactionType: String, ) { + @JsonClass(generateAdapter = true) data class Request( @Json(name = "billing_amount") val billingAmount: BigDecimal, diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index fb3e3e15a1..a4bc580673 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -71,12 +71,15 @@ private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, b when (buildType) { BuildType.Release -> { isDebuggable = false - isMinifyEnabled = false - proguardFiles(appExtension.getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") } BuildType.Debug -> { isDebuggable = true - isMinifyEnabled = false + } + BuildType.DebugPG -> { + // build is not debuggable to let R8 make optimizations + // otherwise, only shrinking will be applied + matchingFallbacks.add(BuildType.Debug.id) + signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) } BuildType.Internal, BuildType.External, @@ -95,6 +98,9 @@ private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, b versionNameSuffix = buildType.versionSuffix?.let { "-$it" } applicationIdSuffix = buildType.appIdSuffix?.let { ".$it" } + isMinifyEnabled = buildType.obfuscating + isShrinkResources = buildType.obfuscating + proguardFiles("proguard-rules.pro", appExtension.getDefaultProguardFile("proguard-android.txt")) } private fun AppExtension.configurePackagingOptions() { @@ -104,6 +110,7 @@ private fun AppExtension.configurePackagingOptions() { excludes += "lib/x86_64/freebsd/libscrypt.so" excludes += "lib/x86_64/linux/libscrypt.so" excludes += "META-INF/gradle/incremental.annotation.processors" + pickFirsts += "google/protobuf/*" merges += "paymentrequest.proto" } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/DependencyHandler.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/DependencyHandler.kt new file mode 100644 index 0000000000..c6a34d5600 --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/DependencyHandler.kt @@ -0,0 +1,25 @@ +package com.tangem.plugin.configuration.configurations.extension + +import com.android.build.gradle.internal.plugins.AppPlugin +import com.android.build.gradle.internal.plugins.LibraryPlugin +import com.tangem.plugin.configuration.model.BuildType +import org.gradle.api.Project + +fun Project.kaptForObfuscatingVariants(dependencyNotation: Any) { + plugins.all { + when (this) { + is AppPlugin -> { + BuildType.values().filter { it.obfuscating }.forEach { + dependencies.add(it.createKaptConfiguration(), dependencyNotation) + } + } + is LibraryPlugin -> { + BuildType.values().filter { it.obfuscating }.forEach { + dependencies.add(it.createKaptConfiguration(), dependencyNotation) + } + } + } + } +} + +private fun BuildType.createKaptConfiguration() = "kapt" + name.replaceFirstChar(Char::titlecase) \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt index f82cfcf71e..572a39af72 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt @@ -45,6 +45,7 @@ private fun LibraryExtension.configurePackagingOptions() { excludes += "lib/x86_64/freebsd/libscrypt.so" excludes += "lib/x86_64/linux/libscrypt.so" excludes += "META-INF/gradle/incremental.annotation.processors" + pickFirsts += "google/protobuf/*" } } } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index 27271c7cea..1ca95f8b20 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -4,6 +4,7 @@ internal enum class BuildType( val id: String, val appIdSuffix: String? = null, val versionSuffix: String? = null, + val obfuscating: Boolean = false, val configFields: List, ) { @@ -31,6 +32,19 @@ internal enum class BuildType( ), ), + DebugPG( + id = "debugPG", + appIdSuffix = "debug", + obfuscating = true, + configFields = listOf( + BuildConfigField.Environment(value = "dev"), + BuildConfigField.TestActionEnabled(isEnabled = true), + BuildConfigField.LogEnabled(isEnabled = true), + BuildConfigField.TesterMenuAvailability(isEnabled = true), + BuildConfigField.MockDataSource(isEnabled = false), + ), + ), + /** * Build type for QA and business * diff --git a/version.properties b/version.properties index b87bff9bef..60c5fb9778 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -versionName=5.20.0 \ No newline at end of file +versionName=5.21.0 \ No newline at end of file