diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 4fa62fd751..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.211.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..6f955d192f 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 72d9d4b3c3f8e6604078798c4b5567327ed9b48a +Subproject commit 6f955d192f7edd876b9f096a30431adeb1594aeb 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/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/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/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/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/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..de076e5379 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,11 +21,11 @@ }, { "name": "ONBOARDING_CODE_REFACTORING_ENABLED", - "version": "undefined" + "version": "5.21.0" }, { "name": "NAVIGATION_REFACTORING", - "version": "undefined" + "version": "5.21.0" }, { "name": "SWAP_STORIES_ENABLED", @@ -34,5 +34,9 @@ { "name": "VISA_ONBOARDING_ENABLED", "version": "undefined" + }, + { + "name": "ONRAMP_HOT_TOKENS_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..486381bfcc 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, 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..5411bcc840 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, @@ -153,6 +153,7 @@ data class YieldDTO( val enabled: Boolean?, ) + @JsonClass(generateAdapter = false) enum class RewardScheduleDTO { @Json(name = "block") BLOCK, @@ -178,6 +179,7 @@ data class YieldDTO( UNKNOWN, } + @JsonClass(generateAdapter = false) enum class RewardClaimingDTO { @Json(name = "auto") AUTO, @@ -189,6 +191,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..4a8260bf8d --- /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 == 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/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/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/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/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/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/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/markets/build.gradle.kts b/data/markets/build.gradle.kts index 632a570f78..e52469954f 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) @@ -39,5 +41,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/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..1742009b08 --- /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 = 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..e793d36c78 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) @@ -45,6 +47,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/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/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 36fc695b7f..80a7803738 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 { @@ -601,7 +601,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 +623,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/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/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/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/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/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/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/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/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/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/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/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/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/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/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..1fb61151c2 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 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/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/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/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/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 55f5e6eea3..bbf66619e0 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) @@ -51,4 +53,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..161326f0bb 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,67 @@ package com.tangem.feature.swap.domain.models.domain +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@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, ) +@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, } \ 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..597b837eac 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 } 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/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/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/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/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index d3ffbeb33b..cb86b9d766 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 @@ -807,7 +807,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 +820,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, ), ) 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/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/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/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 2a7ce1423f..1e608b9662 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-930" +tangemBlockchainSdk = "develop-933" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.20-427" +tangemCardSdk = "develop-429" #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..bdbb9dde4f 100644 --- a/libs/blockchain-sdk/build.gradle.kts +++ b/libs/blockchain-sdk/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,6 +33,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/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/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